fix(swarmtop): expose and render full peer identity/details from proc surfaces

This commit is contained in:
Raven Scott
2026-04-26 08:02:37 -04:00
parent 8c3d5f8795
commit 194fbd2c67
69 changed files with 13463 additions and 32 deletions
+179
View File
@@ -1276,6 +1276,90 @@ async function executeKernel(disk, store, swarm, initSource) {
}
return `${JSON.stringify(o)}\n`
},
procBareOsDhtScanText() {
const peers = disk.peers?.size ?? 0
const dhtStatus = (() => {
try {
return JSON.parse(procBareOsDhtStatusText() || '{}')
} catch {
return {}
}
})()
return `${JSON.stringify({
schema: 1,
atMs: Date.now(),
peers,
dhtStatus,
bootstrapHint: shellEnv.BARE_OS_HYPERDHT_BOOTSTRAP || '',
note: 'Compact DHT inspector surface for dhtscan(1).'
})}\n`
},
procBareOsSwarmDoctorText() {
const peers = disk.peers?.size ?? 0
const chatRx =
typeof disk.protomuxChatChannelRxTotal === 'number'
? disk.protomuxChatChannelRxTotal
: 0
const meshdropRx =
typeof disk.protomuxMeshdropChannelRxTotal === 'number'
? disk.protomuxMeshdropChannelRxTotal
: 0
const health = peers > 0 ? 'ok' : 'degraded'
return `${JSON.stringify({
schema: 1,
atMs: Date.now(),
health,
swarmPeers: peers,
protomuxChatRxTotal: chatRx,
protomuxMeshdropRxTotal: meshdropRx,
remediation:
peers > 0
? []
: [
'verify seeder online',
'check topic/bootstrap',
'inspect BARE_OS_SWARM_MAX_* limits'
]
})}\n`
},
procBareOsRouteSummaryText() {
const peers = disk.peers ? [...disk.peers] : []
const relayLikely = peers.filter(
(p) => p && p.socket && p.socket.remoteAddress == null
).length
return `${JSON.stringify({
schema: 1,
atMs: Date.now(),
peers: peers.length,
directLikely: Math.max(0, peers.length - relayLikely),
relayLikely,
note: 'Heuristic route summary for routeview(1).'
})}\n`
},
procBareOsHolepunchSummaryText() {
const peers = disk.peers?.size ?? 0
const dhtStatus = (() => {
try {
return JSON.parse(procBareOsDhtStatusText() || '{}')
} catch {
return {}
}
})()
return `${JSON.stringify({
schema: 1,
atMs: Date.now(),
peers,
firewalled:
dhtStatus && typeof dhtStatus === 'object'
? dhtStatus.firewalled ?? null
: null,
randomized:
dhtStatus && typeof dhtStatus === 'object'
? dhtStatus.randomized ?? null
: null,
note: 'Holepunch/NAT summary for holepunch-view(1).'
})}\n`
},
procBareOsClockText() {
const wallMs = Date.now()
let monotonicNsFromPerf = null
@@ -2223,10 +2307,50 @@ async function executeKernel(disk, store, swarm, initSource) {
procBareOsSwarmText() {
const tk = topicKey()
const peers = disk.peers?.size ?? 0
const peerRows = (() => {
const rows = []
for (const p of disk.peers ? [...disk.peers] : []) {
const sock = p && typeof p === 'object' ? p.socket : null
const remotePublicKeyHex =
sock && sock.remotePublicKey && sock.remotePublicKey.byteLength === 32
? b4a.toString(sock.remotePublicKey, 'hex')
: null
const handshakeHashHex =
sock && sock.handshakeHash && sock.handshakeHash.byteLength > 0
? b4a.toString(sock.handshakeHash, 'hex')
: null
rows.push({
peerId: typeof p?.id === 'string' ? p.id : null,
remotePublicKey: remotePublicKeyHex,
handshakeHash: handshakeHashHex,
remoteAddress:
sock && typeof sock.remoteAddress === 'string'
? sock.remoteAddress
: null,
remotePort:
sock && typeof sock.remotePort === 'number' ? sock.remotePort : null,
state:
p &&
p.mux &&
p.mux.stream &&
p.mux.stream.destroyed === true
? 'closed'
: 'connected',
channels: {
bareOsV1: !!p?.chan,
chat: !!p?.chatChan,
meshdrop: !!p?.meshdropChan
}
})
}
return rows
})()
const now = Date.now()
return `${JSON.stringify({
topicHex: b4a.toString(tk, 'hex'),
peerCount: peers,
topicCount: 1,
peers: peerRows,
protocol: 'bare-os-v1',
connectionBudget:
disk.swarmConnectionBudget &&
@@ -2331,6 +2455,51 @@ async function executeKernel(disk, store, swarm, initSource) {
})()
})}\n`
},
procBareOsPeerDetailsText() {
const peerRows = (() => {
const rows = []
for (const p of disk.peers ? [...disk.peers] : []) {
const sock = p && typeof p === 'object' ? p.socket : null
const remotePublicKeyHex =
sock && sock.remotePublicKey && sock.remotePublicKey.byteLength === 32
? b4a.toString(sock.remotePublicKey, 'hex')
: null
rows.push({
peerId: typeof p?.id === 'string' ? p.id : null,
remotePublicKey: remotePublicKeyHex,
endpoint: {
address:
sock && typeof sock.remoteAddress === 'string'
? sock.remoteAddress
: null,
port:
sock && typeof sock.remotePort === 'number'
? sock.remotePort
: null
},
transport: {
protocol: 'bare-os-v1',
chatChannel: !!p?.chatChan,
meshdropChannel: !!p?.meshdropChan
},
peerOs: {
osHint: null,
runtimeHint: null,
booterVersionHint: null,
capabilityHint: null,
note: 'Peer OS hints are best-effort and may be null when remote metadata is unavailable.'
}
})
}
return rows
})()
return `${JSON.stringify({
schema: 1,
atMs: Date.now(),
peerCount: peerRows.length,
peers: peerRows
})}\n`
},
procBareOsSwarmHealthText() {
const peers = disk.peers?.size ?? 0
const km = bareOsKernelMetricsSnapshot()
@@ -8520,6 +8689,11 @@ async function executeKernel(disk, store, swarm, initSource) {
['syscalls', '/proc/bare_os/syscalls.json'],
['metricsProm', '/proc/bare_os/metrics.prom'],
['meshdrop', '/proc/bare_os/meshdrop.json'],
['peerDetails', '/proc/bare_os/peer_details.json'],
['dhtScan', '/proc/bare_os/dht_scan.json'],
['swarmDoctor', '/proc/bare_os/swarm_doctor.json'],
['routeSummary', '/proc/bare_os/route_summary.json'],
['holepunchSummary', '/proc/bare_os/holepunch_summary.json'],
['protomuxWire', '/proc/bare_os/protomux.json'],
['securityPosture', '/proc/bare_os/security_posture.json'],
['processIo', '/proc/bare_os/process_io.json'],
@@ -8533,6 +8707,11 @@ async function executeKernel(disk, store, swarm, initSource) {
['replication', '/proc/bare_os/replication'],
['swarm', '/proc/bare_os/swarm'],
['meshdrop', '/proc/bare_os/meshdrop.json'],
['peerDetails', '/proc/bare_os/peer_details.json'],
['dhtScan', '/proc/bare_os/dht_scan.json'],
['swarmDoctor', '/proc/bare_os/swarm_doctor.json'],
['routeSummary', '/proc/bare_os/route_summary.json'],
['holepunchSummary', '/proc/bare_os/holepunch_summary.json'],
['syncWindow', '/proc/bare_os/sync_window.json'],
['processTable', '/proc/bare_os/process_table.json'],
['snapshotHints', '/proc/bare_os/snapshot_hints.json']
+122
View File
@@ -366,6 +366,11 @@ export const BARE_OS_PROC_FILE_TO_ID_HYPERCORE_PACK_HRPC_LIFECYCLE = Object.free
* procBareOsMetricsLiveText?: () => string,
* procBareOsChatText?: () => string,
* procBareOsMeshdropText?: () => string,
* procBareOsPeerDetailsText?: () => string,
* procBareOsDhtScanText?: () => string,
* procBareOsSwarmDoctorText?: () => string,
* procBareOsRouteSummaryText?: () => string,
* procBareOsHolepunchSummaryText?: () => string,
* procBareOsProcessTableText?: () => string,
* procBareOsProcessIoText?: () => string,
* procBareOsProcessThreadsText?: () => string,
@@ -593,6 +598,26 @@ export function createVfs(
typeof vfsOptions.procBareOsMeshdropText === 'function'
? vfsOptions.procBareOsMeshdropText
: null
const procBareOsPeerDetailsText =
typeof vfsOptions.procBareOsPeerDetailsText === 'function'
? vfsOptions.procBareOsPeerDetailsText
: null
const procBareOsDhtScanText =
typeof vfsOptions.procBareOsDhtScanText === 'function'
? vfsOptions.procBareOsDhtScanText
: null
const procBareOsSwarmDoctorText =
typeof vfsOptions.procBareOsSwarmDoctorText === 'function'
? vfsOptions.procBareOsSwarmDoctorText
: null
const procBareOsRouteSummaryText =
typeof vfsOptions.procBareOsRouteSummaryText === 'function'
? vfsOptions.procBareOsRouteSummaryText
: null
const procBareOsHolepunchSummaryText =
typeof vfsOptions.procBareOsHolepunchSummaryText === 'function'
? vfsOptions.procBareOsHolepunchSummaryText
: null
const procBareOsProcessTableText =
typeof vfsOptions.procBareOsProcessTableText === 'function'
? vfsOptions.procBareOsProcessTableText
@@ -1596,6 +1621,28 @@ export function createVfs(
const t = procBareOsMeshdropText ? procBareOsMeshdropText() : '{}\n'
return utf8Encode(t)
}
if (f === 'bare_os_peer_details') {
const t = procBareOsPeerDetailsText ? procBareOsPeerDetailsText() : '{}\n'
return utf8Encode(t)
}
if (f === 'bare_os_dht_scan') {
const t = procBareOsDhtScanText ? procBareOsDhtScanText() : '{}\n'
return utf8Encode(t)
}
if (f === 'bare_os_swarm_doctor') {
const t = procBareOsSwarmDoctorText ? procBareOsSwarmDoctorText() : '{}\n'
return utf8Encode(t)
}
if (f === 'bare_os_route_summary') {
const t = procBareOsRouteSummaryText ? procBareOsRouteSummaryText() : '{}\n'
return utf8Encode(t)
}
if (f === 'bare_os_holepunch_summary') {
const t = procBareOsHolepunchSummaryText
? procBareOsHolepunchSummaryText()
: '{}\n'
return utf8Encode(t)
}
if (f === 'bare_os_process_table') {
const t = procBareOsProcessTableText
? procBareOsProcessTableText()
@@ -1887,6 +1934,14 @@ export function createVfs(
},
{ name: 'chat.json', path: '/proc/bare_os/chat.json' },
{ name: 'meshdrop.json', path: '/proc/bare_os/meshdrop.json' },
{ name: 'peer_details.json', path: '/proc/bare_os/peer_details.json' },
{ name: 'dht_scan.json', path: '/proc/bare_os/dht_scan.json' },
{ name: 'swarm_doctor.json', path: '/proc/bare_os/swarm_doctor.json' },
{ name: 'route_summary.json', path: '/proc/bare_os/route_summary.json' },
{
name: 'holepunch_summary.json',
path: '/proc/bare_os/holepunch_summary.json'
},
{
name: 'process_table.json',
path: '/proc/bare_os/process_table.json'
@@ -2638,6 +2693,16 @@ export function createVfs(
'chat.json': 'bare_os_chat',
meshdrop: 'bare_os_meshdrop',
'meshdrop.json': 'bare_os_meshdrop',
peer_details: 'bare_os_peer_details',
'peer_details.json': 'bare_os_peer_details',
dht_scan: 'bare_os_dht_scan',
'dht_scan.json': 'bare_os_dht_scan',
swarm_doctor: 'bare_os_swarm_doctor',
'swarm_doctor.json': 'bare_os_swarm_doctor',
route_summary: 'bare_os_route_summary',
'route_summary.json': 'bare_os_route_summary',
holepunch_summary: 'bare_os_holepunch_summary',
'holepunch_summary.json': 'bare_os_holepunch_summary',
process_table: 'bare_os_process_table',
'process_table.json': 'bare_os_process_table',
process_io: 'bare_os_process_io',
@@ -3220,6 +3285,58 @@ export function createVfs(
file: 'bare_os_meshdrop'
}
}
if (
sub === 'bare_os_peer_details' ||
sub === 'bare_os_peer_details.json'
) {
return {
virtualPseudo: true,
kind: 'proc',
node: 'file',
file: 'bare_os_peer_details'
}
}
if (sub === 'bare_os_dht_scan' || sub === 'bare_os_dht_scan.json') {
return {
virtualPseudo: true,
kind: 'proc',
node: 'file',
file: 'bare_os_dht_scan'
}
}
if (
sub === 'bare_os_swarm_doctor' ||
sub === 'bare_os_swarm_doctor.json'
) {
return {
virtualPseudo: true,
kind: 'proc',
node: 'file',
file: 'bare_os_swarm_doctor'
}
}
if (
sub === 'bare_os_route_summary' ||
sub === 'bare_os_route_summary.json'
) {
return {
virtualPseudo: true,
kind: 'proc',
node: 'file',
file: 'bare_os_route_summary'
}
}
if (
sub === 'bare_os_holepunch_summary' ||
sub === 'bare_os_holepunch_summary.json'
) {
return {
virtualPseudo: true,
kind: 'proc',
node: 'file',
file: 'bare_os_holepunch_summary'
}
}
if (
sub === 'bare_os_metrics_prom' ||
sub === 'bare_os_metrics_prom.json'
@@ -4758,6 +4875,11 @@ export function createVfs(
'bare_os_boot_budget_summary.json',
'bare_os_chat.json',
'bare_os_meshdrop.json',
'bare_os_peer_details.json',
'bare_os_dht_scan.json',
'bare_os_swarm_doctor.json',
'bare_os_route_summary.json',
'bare_os_holepunch_summary.json',
'bare_os_initd_graph.json',
'bare_os_ipc_backpressure.json',
'bare_os_kernel_program.json',
+1 -1
View File
@@ -8,7 +8,7 @@
"start": "node ../../scripts/ensure-pear-node-modules.mjs packages/bare-os-booter && bare index.js",
"dev": "node ../../scripts/ensure-pear-node-modules.mjs packages/bare-os-booter && bare index.js",
"pear:dev": "node ../../scripts/ensure-pear-node-modules.mjs packages/bare-os-booter && pear run --dev .",
"test": "../../node_modules/.bin/brittle-bare test.identity.js && node --require ./scripts/bare-node-test-shim.cjs ../../node_modules/.bin/brittle-node test.js test.bare-os-chat-service.js test.bare-os-chat-wire.js test.bare-os-chat-roundtrip.js test.bare-holesail.js test.bare-holesail-managed.js test.bare-os-www.js test.bare-os-bind-fallback.js test.bare-os-ssh-holesail.js test.bare-openssh-pty.js test.bare-openssh-sftp.js test.hrpc-allowlist.js test.socket-scm-rights.js test.peer-system-seed.js",
"test": "../../node_modules/.bin/brittle-bare test.identity.js && node --require ./scripts/bare-node-test-shim.cjs ../../node_modules/.bin/brittle-node test.js test.bare-os-chat-service.js test.bare-os-chat-wire.js test.bare-os-chat-roundtrip.js test.bare-holesail.js test.bare-holesail-managed.js test.bare-os-www.js test.bare-os-bind-fallback.js test.bare-os-ssh-holesail.js test.bare-openssh-pty.js test.bare-openssh-sftp.js test.hrpc-allowlist.js test.socket-scm-rights.js test.peer-system-seed.js test.peer-visibility-proc.js",
"test:bare": "../../node_modules/.bin/brittle-bare test.identity.js test.bare-smoke.js",
"test:peer-seed": "node --require ./scripts/bare-node-test-shim.cjs ../../node_modules/.bin/brittle-node test.peer-system-seed.js",
"test:holesail-bare": "bare scripts/holesail-seed-determinism.mjs",
@@ -0,0 +1,21 @@
import test from 'brittle'
import { readFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const booterIndex = join(__dirname, 'index.js')
const vfsPath = join(__dirname, 'lib/vfs.js')
test('booter exposes peer details proc producers', async (t) => {
const src = await readFile(booterIndex, 'utf8')
t.ok(src.includes('procBareOsPeerDetailsText()'), 'peer details proc producer exists')
t.ok(src.includes("['peerDetails', '/proc/bare_os/peer_details.json']"), 'baretop snapshot includes peer details')
t.ok(src.includes('remotePublicKey'), 'swarm proc includes full remote public keys')
})
test('vfs routes peer_details proc aliases', async (t) => {
const src = await readFile(vfsPath, 'utf8')
t.ok(src.includes('bare_os_peer_details'), 'peer_details pseudo file handler exists')
t.ok(src.includes('peer_details.json'), 'peer_details appears in proc index aliases')
})
+34
View File
@@ -57,6 +57,40 @@ const preamble = {
'p2p-suite.js',
'p2p-suite-tui.js'
],
dhttop: [
'edit-ansi.js',
'edit-key-parse.js',
'edit-stream-read.js',
'p2p-suite.js',
'p2p-suite-tui.js'
],
swarmmap: [
'edit-ansi.js',
'edit-key-parse.js',
'edit-stream-read.js',
'p2p-suite.js',
'p2p-suite-tui.js'
],
routeview: [
'edit-ansi.js',
'edit-key-parse.js',
'edit-stream-read.js',
'p2p-suite.js',
'p2p-suite-tui.js'
],
'holepunch-view': [
'edit-ansi.js',
'edit-key-parse.js',
'edit-stream-read.js',
'p2p-suite.js',
'p2p-suite-tui.js'
],
p2ping: ['p2p-suite.js'],
dhtscan: ['p2p-suite.js'],
swarmdoctor: ['p2p-suite.js'],
peerdiscover: ['p2p-suite.js'],
p2ptrace: ['p2p-suite.js'],
peerctl: ['p2p-suite.js'],
meshdrop: ['p2p-suite.js'],
taskmesh: ['p2p-suite.js'],
peernote: ['p2p-suite.js'],
@@ -33,6 +33,8 @@ export const COREUTILS_COMMANDS = [
'dir',
'dirname',
'dircolors',
'dhtscan',
'dhttop',
'du',
'edit',
'echo',
@@ -54,6 +56,7 @@ export const COREUTILS_COMMANDS = [
'hdms',
'help',
'holesail',
'holepunch-view',
'hostid',
'hostname',
'hypershell-board',
@@ -99,6 +102,10 @@ export const COREUTILS_COMMANDS = [
'patch',
'pathchk',
'pathcap-verify',
'p2ping',
'p2ptrace',
'peerctl',
'peerdiscover',
'peernote',
'pr',
'printenv',
@@ -112,6 +119,7 @@ export const COREUTILS_COMMANDS = [
'rev',
'rm',
'rmdir',
'routeview',
'savevault',
'say',
'sed',
@@ -131,6 +139,8 @@ export const COREUTILS_COMMANDS = [
'sshd',
'stat',
'swarmtop',
'swarmdoctor',
'swarmmap',
'sum',
'sync',
'tar',
+28 -1
View File
@@ -208,6 +208,33 @@ async function bareP2pReadSwarmSnapshot(ctx) {
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
out.peers = peers.slice(0, 128)
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
@@ -0,0 +1,9 @@
{
"name": "dhtscan",
"section": 1,
"title": "dht scan summary",
"synopsis": ["dhtscan"],
"description": "Reads and prints compact DHT diagnostics from /proc/bare_os/dht_scan.json.",
"options": [],
"keywords": ["p2p", "dht", "diagnostics"]
}
@@ -0,0 +1,9 @@
{
"name": "dhttop",
"section": 1,
"title": "live dht tui",
"synopsis": ["dhttop"],
"description": "TTY dashboard for DHT status and bootstrap/firewall hints.",
"options": [],
"keywords": ["p2p", "dht", "tui"]
}
@@ -0,0 +1,9 @@
{
"name": "holepunch-view",
"section": 1,
"title": "holepunch and nat tui",
"synopsis": ["holepunch-view"],
"description": "Displays NAT and holepunch summary telemetry for the current swarm session.",
"options": [],
"keywords": ["p2p", "nat", "holepunch", "tui"]
}
@@ -0,0 +1,9 @@
{
"name": "p2ping",
"section": 1,
"title": "p2p ping probe",
"synopsis": ["p2ping [LABEL]"],
"description": "Emits a p2p ping event and prints local latency and swarm peer hints.",
"options": [],
"keywords": ["p2p", "ping", "latency", "swarm"]
}
@@ -0,0 +1,9 @@
{
"name": "p2ptrace",
"section": 1,
"title": "p2p trace collector",
"synopsis": ["p2ptrace [APP] [N]"],
"description": "Dumps recent p2p events as JSON lines for tracing and diagnostics.",
"options": [],
"keywords": ["p2p", "trace", "json"]
}
@@ -0,0 +1,9 @@
{
"name": "peerctl",
"section": 1,
"title": "peer control plane utility",
"synopsis": ["peerctl health", "peerctl request ACTION [JSON_PAYLOAD]", "peerctl history [N]"],
"description": "P2P control helper for health checks and request envelopes.",
"options": [],
"keywords": ["p2p", "control", "rpc"]
}
@@ -0,0 +1,9 @@
{
"name": "peerdiscover",
"section": 1,
"title": "peer service discovery utility",
"synopsis": ["peerdiscover announce SERVICE [META]", "peerdiscover list [N]"],
"description": "Announces and lists peer service metadata over p2p event history.",
"options": [],
"keywords": ["p2p", "discovery", "service"]
}
@@ -0,0 +1,9 @@
{
"name": "routeview",
"section": 1,
"title": "route visibility tui",
"synopsis": ["routeview"],
"description": "Shows direct-vs-relay route summary from /proc/bare_os/route_summary.json.",
"options": [],
"keywords": ["p2p", "routing", "relay", "tui"]
}
@@ -0,0 +1,9 @@
{
"name": "swarmdoctor",
"section": 1,
"title": "swarm diagnostic doctor",
"synopsis": ["swarmdoctor"],
"description": "Reads /proc/bare_os/swarm_doctor.json and reports swarm health/remediation.",
"options": [],
"keywords": ["p2p", "swarm", "doctor"]
}
@@ -0,0 +1,9 @@
{
"name": "swarmmap",
"section": 1,
"title": "swarm topology map tui",
"synopsis": ["swarmmap"],
"description": "Live peer topology viewer for swarm peers and states.",
"options": [],
"keywords": ["p2p", "swarm", "tui", "topology"]
}
+1 -1
View File
@@ -6,6 +6,6 @@
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
"scripts": {
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
"test": "node ./test/clear-sequence.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs"
"test": "node ./test/clear-sequence.test.mjs && node ./test/help-bin-list.test.mjs && node ./test/whois-rdap.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baresay-say-bundle.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs && node ./test/sha224-sha384-sum.test.mjs && node ./test/agent-sse-parse.test.mjs && node ./test/agent-web-fetch.test.mjs && node ./test/agent-helpers.test.mjs && node ./test/agent-workspace.test.mjs && node ./test/agent-skills.test.mjs && node ./test/agent-config-surface.test.mjs && node ./test/telnet-protocol.test.mjs && node ./test/telnet-cli.test.mjs && node ./test/login.test.mjs && node ./test/p2p-suite-bundles.test.mjs && node ./test/swarmtop-peer-visibility.test.mjs"
}
}
+16
View File
@@ -0,0 +1,16 @@
async function run(ctx, argv) {
const argv0 = argv[0] || 'dhtscan'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' +
argv0 +
'\n' +
'Print compact DHT scan summary from /proc/bare_os/dht_scan.json.\n' +
'See man dhtscan.'
)
return
}
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
ctx.console.log(JSON.stringify(dht, null, 2))
}
+34
View File
@@ -0,0 +1,34 @@
async function run(ctx, argv) {
const argv0 = argv[0] || 'dhttop'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' + argv0 + '\n' + 'Live DHT dashboard TUI.\n' + 'See man dhttop.'
)
return
}
const stdin = ctx.replStdin
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
if (!isTTY) {
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
ctx.console.log(JSON.stringify(dht, null, 2))
return
}
await bareP2pRunSimpleTui(ctx, {
title: 'dhttop',
subtitle: 'dht operations · q quit · r refresh',
renderLines: async () => {
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
const lines = []
lines.push('schema: ' + String(dht.schema ?? '?'))
lines.push('peers: ' + String(dht.peers ?? '?'))
lines.push('firewalled: ' + String(dht.dhtStatus?.firewalled ?? '?'))
lines.push('randomized: ' + String(dht.dhtStatus?.randomized ?? '?'))
lines.push('bootstrap: ' + String(dht.bootstrapHint || '(default)'))
lines.push('')
lines.push('raw:')
lines.push(JSON.stringify(dht))
return lines
}
})
}
@@ -0,0 +1,35 @@
async function run(ctx, argv) {
const argv0 = argv[0] || 'holepunch-view'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' +
argv0 +
'\n' +
'NAT and holepunch summary dashboard.\n' +
'See man holepunch-view.'
)
return
}
const stdin = ctx.replStdin
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
if (!isTTY) {
const h = await bareP2pReadProcJson(ctx, '/proc/bare_os/holepunch_summary.json')
ctx.console.log(JSON.stringify(h, null, 2))
return
}
await bareP2pRunSimpleTui(ctx, {
title: 'holepunch-view',
subtitle: 'nat and holepunch telemetry · q quit · r refresh',
renderLines: async () => {
const h = await bareP2pReadProcJson(ctx, '/proc/bare_os/holepunch_summary.json')
return [
'peers: ' + String(h.peers ?? '?'),
'firewalled: ' + String(h.firewalled ?? '?'),
'randomized: ' + String(h.randomized ?? '?'),
'',
'note: ' + String(h.note || '')
]
}
})
}
+36
View File
@@ -0,0 +1,36 @@
async function run(ctx, argv) {
const argv0 = argv[0] || 'p2ping'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' +
argv0 +
' [LABEL]\n' +
'Emit a p2p ping event and print local swarm timing hints.\n' +
'See man p2ping.'
)
return
}
const label = args.join(' ').trim() || 'ping'
const t0 = Date.now()
const swarm = await bareP2pReadSwarmSnapshot(ctx)
const evtId = bareP2pId('p2ping')
const send = bareP2pSend(ctx, 'p2ping', 'ping', {
evtId,
label,
sentAtMs: t0
})
const t1 = Date.now()
const out = {
schema: 1,
tool: argv0,
evtId,
label,
ok: !(send && send.ok === false),
sendReason: send && send.ok === false ? String(send.reason || 'send_failed') : null,
localElapsedMs: t1 - t0,
swarmPeers: swarm.peerCount
}
ctx.console.log(JSON.stringify(out, null, 2))
if (send && send.ok === false) ctx.exitCode = 1
}
@@ -0,0 +1,45 @@
async function run(ctx, argv) {
const argv0 = argv[0] || 'p2ptrace'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' +
argv0 +
' [APP] [N]\n' +
'Dump recent p2p events as JSON lines.\n' +
'APP defaults to all known p2p apps.\n' +
'See man p2ptrace.'
)
return
}
const app = String(args[0] || '').trim()
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '200', 10) || 200))
const apps = app
? [app]
: [
'swarmtop',
'meshdrop',
'taskmesh',
'peernote',
'hypershell-board',
'p2ping',
'peerdiscover',
'peerctl'
]
const rows = []
for (const a of apps) {
rows.push(...bareP2pCollectFromHistory(ctx, a, n))
}
rows.sort((a, b) => (a.receivedAtMs || 0) - (b.receivedAtMs || 0))
for (const r of rows.slice(-n)) {
ctx.console.log(
JSON.stringify({
atMs: r.receivedAtMs || 0,
app: r.packet?.app || '?',
kind: r.packet?.kind || '?',
from: r.displayName || r.fromPeerKey || '',
payload: r.packet?.payload || {}
})
)
}
}
+81
View File
@@ -0,0 +1,81 @@
async function run(ctx, argv) {
const argv0 = argv[0] || 'peerctl'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
ctx.console.log(
'usage: ' +
argv0 +
' health\n' +
' ' +
argv0 +
' request ACTION [JSON_PAYLOAD]\n' +
' ' +
argv0 +
' history [N]\n' +
'P2P control plane helper over event envelopes.\n' +
'See man peerctl.'
)
if (args.length === 0) ctx.exitCode = 1
return
}
const sub = args[0]
if (sub === 'health') {
const swarm = await bareP2pReadSwarmSnapshot(ctx)
const doctor = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
ctx.console.log(
JSON.stringify(
{
schema: 1,
swarmPeers: swarm.peerCount,
health: doctor.health || 'unknown',
remediation: doctor.remediation || []
},
null,
2
)
)
return
}
if (sub === 'request') {
const action = String(args[1] || '').trim()
if (!action) {
ctx.console.error(argv0 + ': request requires ACTION')
ctx.exitCode = 1
return
}
let payload = {}
if (args[2]) {
try {
payload = JSON.parse(args.slice(2).join(' '))
} catch {
ctx.console.error(argv0 + ': invalid JSON payload')
ctx.exitCode = 1
return
}
}
const r = bareP2pSend(ctx, 'peerctl', 'request', {
requestId: bareP2pId('ctl'),
action,
payload
})
if (r && r.ok === false) ctx.exitCode = 1
return
}
if (sub === 'history') {
const n = Math.max(1, Math.min(1000, parseInt(args[1] || '50', 10) || 50))
const rows = bareP2pCollectFromHistory(ctx, 'peerctl', 2000).slice(-n)
for (const row of rows) {
ctx.console.log(
'[' +
bareP2pFmtClock(row.receivedAtMs) +
'] ' +
String(row.packet?.kind || '?') +
' ' +
JSON.stringify(row.packet?.payload || {})
)
}
return
}
ctx.console.error(argv0 + ': unsupported subcommand')
ctx.exitCode = 1
}
@@ -0,0 +1,54 @@
async function run(ctx, argv) {
const argv0 = argv[0] || 'peerdiscover'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
ctx.console.log(
'usage: ' +
argv0 +
' announce SERVICE [META]\n' +
' ' +
argv0 +
' list [N]\n' +
'Peer service announce/list over p2p event history.\n' +
'See man peerdiscover.'
)
if (args.length === 0) ctx.exitCode = 1
return
}
const sub = args[0]
if (sub === 'announce') {
const service = String(args[1] || '').trim()
if (!service) {
ctx.console.error(argv0 + ': announce requires SERVICE')
ctx.exitCode = 1
return
}
const meta = args.slice(2).join(' ').trim()
const r = bareP2pSend(ctx, 'peerdiscover', 'service.announce', {
service,
meta,
id: bareP2pId('svc')
})
if (r && r.ok === false) ctx.exitCode = 1
return
}
if (sub === 'list') {
const n = Math.max(1, Math.min(500, parseInt(args[1] || '50', 10) || 50))
const rows = bareP2pCollectFromHistory(ctx, 'peerdiscover', 2000)
.filter((r) => r.packet?.kind === 'service.announce')
.slice(-n)
for (const row of rows) {
const p = row.packet?.payload || {}
ctx.console.log(
'[' +
bareP2pFmtClock(row.receivedAtMs) +
'] ' +
String(p.service || 'unknown') +
(p.meta ? ' ' + String(p.meta) : '')
)
}
return
}
ctx.console.error(argv0 + ': unsupported subcommand')
ctx.exitCode = 1
}
@@ -0,0 +1,31 @@
async function run(ctx, argv) {
const argv0 = argv[0] || 'routeview'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' + argv0 + '\n' + 'Route and relay visibility TUI.\n' + 'See man routeview.'
)
return
}
const stdin = ctx.replStdin
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
if (!isTTY) {
const r = await bareP2pReadProcJson(ctx, '/proc/bare_os/route_summary.json')
ctx.console.log(JSON.stringify(r, null, 2))
return
}
await bareP2pRunSimpleTui(ctx, {
title: 'routeview',
subtitle: 'direct vs relay summary · q quit · r refresh',
renderLines: async () => {
const r = await bareP2pReadProcJson(ctx, '/proc/bare_os/route_summary.json')
return [
'peers: ' + String(r.peers ?? '?'),
'directLikely: ' + String(r.directLikely ?? '?'),
'relayLikely: ' + String(r.relayLikely ?? '?'),
'',
'note: ' + String(r.note || '')
]
}
})
}
@@ -0,0 +1,19 @@
async function run(ctx, argv) {
const argv0 = argv[0] || 'swarmdoctor'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' +
argv0 +
'\n' +
'Print swarm diagnosis from /proc/bare_os/swarm_doctor.json.\n' +
'See man swarmdoctor.'
)
return
}
const doc = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
ctx.console.log(JSON.stringify(doc, null, 2))
if (doc && typeof doc === 'object' && doc.health && doc.health !== 'ok') {
ctx.exitCode = 1
}
}
@@ -0,0 +1,39 @@
async function run(ctx, argv) {
const argv0 = argv[0] || 'swarmmap'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' + argv0 + '\n' + 'Swarm topology map TUI.\n' + 'See man swarmmap.'
)
return
}
const stdin = ctx.replStdin
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
const snap = await bareP2pReadSwarmSnapshot(ctx)
if (!isTTY) {
ctx.console.log(JSON.stringify(snap, null, 2))
return
}
await bareP2pRunSimpleTui(ctx, {
title: 'swarmmap',
subtitle: 'peer topology · q quit · r refresh',
renderLines: async () => {
const s = await bareP2pReadSwarmSnapshot(ctx)
const lines = []
lines.push('peerCount: ' + String(s.peerCount ?? '?'))
lines.push('topicCount: ' + String(s.topicCount ?? '?'))
lines.push('')
lines.push('Peers:')
const peers = Array.isArray(s.peers) ? s.peers : []
if (!peers.length) lines.push(' (none)')
for (const p of peers.slice(0, 48)) {
const id =
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
? p.remotePublicKey.slice(0, 20)
: '?'
lines.push(' ' + id + ' ' + String(p.state || 'connected'))
}
return lines
}
})
}
+18 -3
View File
@@ -85,16 +85,31 @@ async function run(ctx, argv) {
lines.push('Peers:')
const peers = Array.isArray(snap.peers) ? snap.peers : []
if (!peers.length) lines.push(' (no peers reported)')
for (const p of peers.slice(0, 24)) {
for (const p of peers.slice(0, 12)) {
const key =
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
? p.remotePublicKey.slice(0, 16)
? p.remotePublicKey
: '?'
const state =
p && typeof p === 'object' && typeof p.state === 'string'
? p.state
: 'connected'
lines.push(' ' + key + ' ' + state)
const addr =
p && typeof p === 'object' && p.endpoint && typeof p.endpoint === 'object'
? String(p.endpoint.address || p.remoteAddress || '?') +
':' +
String(p.endpoint.port || p.remotePort || '?')
: String(p?.remoteAddress || '?') + ':' + String(p?.remotePort || '?')
const osHint =
p &&
typeof p === 'object' &&
p.peerOs &&
typeof p.peerOs === 'object' &&
p.peerOs.osHint
? String(p.peerOs.osHint)
: 'unknown'
lines.push(' pubkey: ' + key)
lines.push(' state=' + state + ' endpoint=' + addr + ' os=' + osHint)
}
lines.push('')
lines.push('Recent events:')
@@ -17,7 +17,17 @@ test('p2p suite commands include shared envelope helper in kernel bins', async (
'meshdrop',
'taskmesh',
'peernote',
'hypershell-board'
'hypershell-board',
'p2ping',
'dhtscan',
'dhttop',
'swarmdoctor',
'swarmmap',
'peerdiscover',
'p2ptrace',
'peerctl',
'routeview',
'holepunch-view'
]
for (const name of names) {
const src = await read(join(kernelBin, name))
@@ -32,7 +42,17 @@ test('p2p suite man pages exist', async (t) => {
'meshdrop',
'taskmesh',
'peernote',
'hypershell-board'
'hypershell-board',
'p2ping',
'dhtscan',
'dhttop',
'swarmdoctor',
'swarmmap',
'peerdiscover',
'p2ptrace',
'peerctl',
'routeview',
'holepunch-view'
]
for (const page of pages) {
const body = await read(join(coreutilsRoot, 'man/pages', page + '.json'))
@@ -0,0 +1,23 @@
import test from 'brittle'
import { readFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const coreutilsRoot = join(__dirname, '..')
test('p2p suite reads peer_details proc for swarm snapshots', async (t) => {
const src = await readFile(join(coreutilsRoot, 'lib/p2p-suite.js'), 'utf8')
t.ok(src.includes('/proc/bare_os/peer_details.json'), 'reads peer details proc')
t.ok(src.includes('out.peers = merged.slice(0, 128)'), 'merges swarm + peer details rows')
})
test('swarmtop renders full public keys and endpoint details', async (t) => {
const src = await readFile(join(coreutilsRoot, 'src/swarmtop.js'), 'utf8')
t.ok(src.includes("lines.push(' pubkey: ' + key)"), 'shows full pubkey label')
t.ok(src.includes('endpoint='), 'shows endpoint details')
t.ok(
src.includes('? p.remotePublicKey') || src.includes('? p.remotePublicKey\n'),
'uses full remotePublicKey value in peer rows'
)
})
+346
View File
@@ -0,0 +1,346 @@
/* 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
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'dhtscan'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' +
argv0 +
'\n' +
'Print compact DHT scan summary from /proc/bare_os/dht_scan.json.\n' +
'See man dhtscan.'
)
return
}
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
ctx.console.log(JSON.stringify(dht, null, 2))
}
+954
View File
@@ -0,0 +1,954 @@
/* 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'
}
/**
* @param {Record<string, unknown>} ctx
* @param {import('stream').Writable} stdout
* @param {string} s
*/
function bareEditWrite(ctx, stdout, s) {
if (!stdout || typeof stdout.write !== 'function') return
try {
stdout.write(s)
} catch {
try {
ctx.console?.error?.('edit: stdout write failed')
} catch {
/* ignore */
}
}
}
/** 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' }
const tildeNum = /^(\d+)~$/.exec(seq)
if (tildeNum) {
const n = parseInt(tildeNum[1], 10)
const fnMap = {
11: 1,
12: 2,
13: 3,
14: 4,
15: 5,
17: 6,
18: 7,
19: 8,
20: 9,
21: 10,
23: 11,
24: 12
}
if (fnMap[n] != null) return { type: 'fn', n: fnMap[n] }
}
return { type: 'unknown' }
}
if (last === 'Z') return { type: 'nav', key: 'stab' }
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' }
if (b3 === 80) return { type: 'fn', n: 1 }
if (b3 === 81) return { type: 'fn', n: 2 }
if (b3 === 82) return { type: 'fn', n: 3 }
if (b3 === 83) return { type: 'fn', n: 4 }
return { type: 'unknown' }
}
/** ESC [ 200 ~ */
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
/** ESC [ 201 ~ */
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
/**
* @param {number[]} q
* @param {number[]} prefix
*/
function bareEditStartsWithBytes(q, prefix) {
if (q.length < prefix.length) return false
for (let i = 0; i < prefix.length; i++) {
if (q[i] !== prefix[i]) return false
}
return true
}
/**
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
* @param {number[]} q
*/
function bareEditCouldBeBracketPastePrefix(q) {
const pre = BARE_EDIT_BRACKET_PASTE_START
const n = Math.min(q.length, pre.length)
for (let i = 0; i < n; i++) {
if (q[i] !== pre[i]) return false
}
return true
}
/**
* @param {number[]} bytes
*/
function bareEditDecodePasteInner(bytes) {
try {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8', { fatal: false }).decode(
new Uint8Array(bytes)
)
}
} catch {
/* fall through */
}
let s = ''
for (const b of bytes) s += String.fromCharCode(b)
return s
}
/**
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
* @param {number[]} q
* @returns {string|undefined}
*/
function bareEditFinalizeBracketedPasteOnEof(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
const inner = q.slice(SL)
q.length = 0
return bareEditDecodePasteInner(inner)
}
return undefined
}
/**
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
* @param {number[]} q mutable queue
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
*/
function bareEditTryConsumeBracketedPaste(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
const EL = BARE_EDIT_BRACKET_PASTE_END.length
if (!q.length) return undefined
if (q.length < SL) {
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
}
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
return undefined
}
for (let i = SL; i <= q.length - EL; i++) {
let ok = true
for (let j = 0; j < EL; j++) {
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
ok = false
break
}
}
if (ok) {
const inner = q.slice(SL, i)
q.splice(0, i + EL)
return bareEditDecodePasteInner(inner)
}
}
return null
}
/**
* @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) }
}
/** Stdin chunk queue + async key reader for edit/chat TUIs (requires edit-key-parse.js first). */
/**
* @param {unknown} chunk
* @returns {number[]}
*/
function bareEditChunkBytes(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
}
/**
* @param {{ nextByte: () => Promise<number|undefined>, dispose?: () => void, _keyq?: number[] }} reader
*/
async function bareEditReadKey(reader) {
reader._keyq = reader._keyq || []
const q = reader._keyq
for (;;) {
const pasteText = bareEditTryConsumeBracketedPaste(q)
if (pasteText !== undefined && pasteText !== null) {
return { type: 'paste', text: pasteText }
}
if (pasteText === null) {
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
return { type: 'key', ch: '\x1b' }
}
const lone = q.shift()
if (lone !== undefined && lone < 0x20) {
return { type: 'ctrl', code: lone }
}
if (lone !== undefined) {
return { type: 'key', ch: String.fromCharCode(lone) }
}
return { type: 'eof' }
}
q.push(b)
continue
}
const ev = bareEditTryConsumeKey(q)
if (ev) return ev
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
return { type: 'key', ch: '\x1b' }
}
const lone = q.shift()
if (lone !== undefined && lone < 0x20) {
return { type: 'ctrl', code: lone }
}
if (lone !== undefined) {
return { type: 'key', ch: String.fromCharCode(lone) }
}
return { type: 'eof' }
}
q.push(b)
}
}
/**
* @param {import('stream').Readable} stdin
*/
function bareEditCreateStdinReader(stdin) {
/** @type {number[]} */
const bytes = []
/** @type {(() => void)[]} */
const waiters = []
function drain() {
while (waiters.length && bytes.length) {
const w = waiters.shift()
if (w) w()
}
}
/** @param {unknown} chunk */
function onData(chunk) {
bytes.push(...bareEditChunkBytes(chunk))
drain()
}
stdin.on('data', onData)
return {
nextByte() {
if (bytes.length) return Promise.resolve(bytes.shift())
return new Promise((resolve) => {
waiters.push(() => resolve(bytes.shift()))
})
},
dispose() {
stdin.removeListener('data', onData)
}
}
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
function bareP2pFmtClock(ms) {
const d = new Date(typeof ms === 'number' ? ms : Date.now())
const z = (n) => (n < 10 ? '0' : '') + n
return z(d.getHours()) + ':' + z(d.getMinutes()) + ':' + z(d.getSeconds())
}
function bareP2pTruncate(s, cols) {
const t = String(s || '')
if (t.length <= cols) return t
return t.slice(0, Math.max(0, cols - 1)) + '\u2026'
}
/**
* Read-only TUI shell: q quits, r refreshes.
* @param {Record<string, unknown>} ctx
* @param {{ title: string, subtitle?: string, renderLines: () => string[] | Promise<string[]>, onRefresh?: () => void | Promise<void> }} opts
*/
async function bareP2pRunSimpleTui(ctx, opts) {
const stdin = ctx.replStdin
const stdout = bareEditResolveStdout(ctx)
if (!stdin || !stdout) {
ctx.console.error('p2p tui: missing stdin/stdout')
ctx.exitCode = 1
return
}
const useColor = bareEditUseColor(ctx)
const envEarly =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
const cols0 = parseInt(envEarly.COLUMNS || '80', 10) || 80
const rows0 = parseInt(envEarly.LINES || '24', 10) || 24
const dims = () => ({
cols:
Math.max(48, /** @type {{ columns?: number }} */ (stdout).columns || cols0),
rows: Math.max(12, /** @type {{ rows?: number }} */ (stdout).rows || rows0)
})
const reader = bareEditCreateStdinReader(stdin)
let suspended = false
let alt = false
let loop = true
const draw = async () => {
const { cols, rows } = dims()
const lines = await opts.renderLines()
let out = '\x1b[?25l\x1b[2J\x1b[H'
const top =
bareEditSgr('status', useColor) +
bareP2pTruncate(' ' + opts.title + ' ', cols) +
EDIT_ANSI_RESET
out += bareEditCup(1, 1) + '\x1b[K' + top
const sub = bareP2pTruncate(
(opts.subtitle || 'q quit · r refresh') + ' · ' + bareP2pFmtClock(Date.now()),
cols
)
out +=
bareEditCup(2, 1) +
'\x1b[K' +
bareEditSgr('dim', useColor) +
sub +
EDIT_ANSI_RESET
out += bareEditCup(3, 1) + '\x1b[K'
const maxBody = Math.max(1, rows - 3)
for (let i = 0; i < maxBody; i++) {
const raw = i < lines.length ? String(lines[i]) : ''
out += bareEditCup(4 + i, 1) + '\x1b[K' + bareP2pTruncate(raw, cols)
}
bareEditWrite(ctx, stdout, out + '\x1b[?25h')
}
try {
if (typeof ctx.suspendReplForSubprocess === 'function') {
ctx.suspendReplForSubprocess()
suspended = true
}
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true)
if (typeof stdin.resume === 'function') stdin.resume()
bareEditWrite(ctx, stdout, '\x1b[?1049h')
alt = true
await draw()
while (loop) {
const ev = await bareEditReadKey(reader)
if (ev.type === 'eof') break
if (ev.type === 'ctrl') {
const code = typeof ev.code === 'number' ? ev.code : 0
if (ev.code === 'interrupt' || code === 3 || code === 17 || code === 24) {
break
}
}
if (ev.type === 'key' && ev.ch) {
if (ev.ch === 'q' || ev.ch === 'Q') break
if (ev.ch === 'r' || ev.ch === 'R') {
if (typeof opts.onRefresh === 'function') await opts.onRefresh()
await draw()
continue
}
}
await draw()
}
} finally {
try {
if (alt) bareEditWrite(ctx, stdout, '\x1b[?1049l')
bareEditWrite(ctx, stdout, '\x1b[?25h\x1b[0m')
} catch {
/* ignore */
}
reader.dispose()
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 argv0 = argv[0] || 'dhttop'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' + argv0 + '\n' + 'Live DHT dashboard TUI.\n' + 'See man dhttop.'
)
return
}
const stdin = ctx.replStdin
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
if (!isTTY) {
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
ctx.console.log(JSON.stringify(dht, null, 2))
return
}
await bareP2pRunSimpleTui(ctx, {
title: 'dhttop',
subtitle: 'dht operations · q quit · r refresh',
renderLines: async () => {
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
const lines = []
lines.push('schema: ' + String(dht.schema ?? '?'))
lines.push('peers: ' + String(dht.peers ?? '?'))
lines.push('firewalled: ' + String(dht.dhtStatus?.firewalled ?? '?'))
lines.push('randomized: ' + String(dht.dhtStatus?.randomized ?? '?'))
lines.push('bootstrap: ' + String(dht.bootstrapHint || '(default)'))
lines.push('')
lines.push('raw:')
lines.push(JSON.stringify(dht))
return lines
}
})
}
+1 -1
View File
@@ -87,7 +87,7 @@ function bareOsEmitRaw(ctx, chunk) {
return false
}
var BARE_OS_HELP_BIN_SPACED = "agent arch awk baresay baretop base32 base64 basename basenc btop bundlebee cat chat chgrp chmod chown cksum clear cmp comm cp crontab curl cut date dd df diff dir dircolors dirname du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holesail hostid hostname hrpc hypershell-board iconv id install join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln logger login logname logout ls man md5sum meshdrop mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl paste patch pathcap-verify pathchk pear-runtime-matrix peernote pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir savevault say sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh-keygen ssh-keygen sshd sshd stat sum swarmtop sync systemctl tac tail tar tar taskmesh tee telnet test theme time timeout touch tr true truncate tsort tty ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami whois xargs xattr yes"
var BARE_OS_HELP_BIN_SPACED = "agent arch awk baresay baretop base32 base64 basename basenc btop bundlebee cat chat chgrp chmod chown cksum clear cmp comm cp crontab curl cut date dd df dhtscan dhttop diff dir dircolors dirname du echo edit env exit expand expr factor false find fmt fold getconf getfacl git git-pear grep groups hdms head help holepunch-view holesail hostid hostname hrpc hypershell-board iconv id install join journalctl jq kernel-boot-diff kernel-doctor kernel-explain kernel-fsck kernel-home-snapshot kernel-manifest-validate kernel-preflight kernel-triage kill link ln logger login logname logout ls man md5sum meshdrop mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl p2ping p2ptrace paste patch pathcap-verify pathchk pear-runtime-matrix peerctl peerdiscover peernote pkg-swarm-index pr printenv printf procstat ps pwd readlink realpath rev rm rmdir routeview savevault say sed seq setfacl sh sha1sum sha224sum sha256sum sha384sum sha512sum shuf sidecar sleep sort split ssh-keygen ssh-keygen sshd sshd stat sum swarmdoctor swarmmap swarmtop sync systemctl tac tail tar tar taskmesh tee telnet test theme time timeout touch tr true truncate tsort tty ulimit umount uname unexpand uniq unlink uptime users vdir wc wget which who whoami whois xargs xattr yes"
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
@@ -0,0 +1,955 @@
/* 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'
}
/**
* @param {Record<string, unknown>} ctx
* @param {import('stream').Writable} stdout
* @param {string} s
*/
function bareEditWrite(ctx, stdout, s) {
if (!stdout || typeof stdout.write !== 'function') return
try {
stdout.write(s)
} catch {
try {
ctx.console?.error?.('edit: stdout write failed')
} catch {
/* ignore */
}
}
}
/** 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' }
const tildeNum = /^(\d+)~$/.exec(seq)
if (tildeNum) {
const n = parseInt(tildeNum[1], 10)
const fnMap = {
11: 1,
12: 2,
13: 3,
14: 4,
15: 5,
17: 6,
18: 7,
19: 8,
20: 9,
21: 10,
23: 11,
24: 12
}
if (fnMap[n] != null) return { type: 'fn', n: fnMap[n] }
}
return { type: 'unknown' }
}
if (last === 'Z') return { type: 'nav', key: 'stab' }
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' }
if (b3 === 80) return { type: 'fn', n: 1 }
if (b3 === 81) return { type: 'fn', n: 2 }
if (b3 === 82) return { type: 'fn', n: 3 }
if (b3 === 83) return { type: 'fn', n: 4 }
return { type: 'unknown' }
}
/** ESC [ 200 ~ */
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
/** ESC [ 201 ~ */
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
/**
* @param {number[]} q
* @param {number[]} prefix
*/
function bareEditStartsWithBytes(q, prefix) {
if (q.length < prefix.length) return false
for (let i = 0; i < prefix.length; i++) {
if (q[i] !== prefix[i]) return false
}
return true
}
/**
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
* @param {number[]} q
*/
function bareEditCouldBeBracketPastePrefix(q) {
const pre = BARE_EDIT_BRACKET_PASTE_START
const n = Math.min(q.length, pre.length)
for (let i = 0; i < n; i++) {
if (q[i] !== pre[i]) return false
}
return true
}
/**
* @param {number[]} bytes
*/
function bareEditDecodePasteInner(bytes) {
try {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8', { fatal: false }).decode(
new Uint8Array(bytes)
)
}
} catch {
/* fall through */
}
let s = ''
for (const b of bytes) s += String.fromCharCode(b)
return s
}
/**
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
* @param {number[]} q
* @returns {string|undefined}
*/
function bareEditFinalizeBracketedPasteOnEof(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
const inner = q.slice(SL)
q.length = 0
return bareEditDecodePasteInner(inner)
}
return undefined
}
/**
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
* @param {number[]} q mutable queue
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
*/
function bareEditTryConsumeBracketedPaste(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
const EL = BARE_EDIT_BRACKET_PASTE_END.length
if (!q.length) return undefined
if (q.length < SL) {
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
}
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
return undefined
}
for (let i = SL; i <= q.length - EL; i++) {
let ok = true
for (let j = 0; j < EL; j++) {
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
ok = false
break
}
}
if (ok) {
const inner = q.slice(SL, i)
q.splice(0, i + EL)
return bareEditDecodePasteInner(inner)
}
}
return null
}
/**
* @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) }
}
/** Stdin chunk queue + async key reader for edit/chat TUIs (requires edit-key-parse.js first). */
/**
* @param {unknown} chunk
* @returns {number[]}
*/
function bareEditChunkBytes(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
}
/**
* @param {{ nextByte: () => Promise<number|undefined>, dispose?: () => void, _keyq?: number[] }} reader
*/
async function bareEditReadKey(reader) {
reader._keyq = reader._keyq || []
const q = reader._keyq
for (;;) {
const pasteText = bareEditTryConsumeBracketedPaste(q)
if (pasteText !== undefined && pasteText !== null) {
return { type: 'paste', text: pasteText }
}
if (pasteText === null) {
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
return { type: 'key', ch: '\x1b' }
}
const lone = q.shift()
if (lone !== undefined && lone < 0x20) {
return { type: 'ctrl', code: lone }
}
if (lone !== undefined) {
return { type: 'key', ch: String.fromCharCode(lone) }
}
return { type: 'eof' }
}
q.push(b)
continue
}
const ev = bareEditTryConsumeKey(q)
if (ev) return ev
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
return { type: 'key', ch: '\x1b' }
}
const lone = q.shift()
if (lone !== undefined && lone < 0x20) {
return { type: 'ctrl', code: lone }
}
if (lone !== undefined) {
return { type: 'key', ch: String.fromCharCode(lone) }
}
return { type: 'eof' }
}
q.push(b)
}
}
/**
* @param {import('stream').Readable} stdin
*/
function bareEditCreateStdinReader(stdin) {
/** @type {number[]} */
const bytes = []
/** @type {(() => void)[]} */
const waiters = []
function drain() {
while (waiters.length && bytes.length) {
const w = waiters.shift()
if (w) w()
}
}
/** @param {unknown} chunk */
function onData(chunk) {
bytes.push(...bareEditChunkBytes(chunk))
drain()
}
stdin.on('data', onData)
return {
nextByte() {
if (bytes.length) return Promise.resolve(bytes.shift())
return new Promise((resolve) => {
waiters.push(() => resolve(bytes.shift()))
})
},
dispose() {
stdin.removeListener('data', onData)
}
}
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
function bareP2pFmtClock(ms) {
const d = new Date(typeof ms === 'number' ? ms : Date.now())
const z = (n) => (n < 10 ? '0' : '') + n
return z(d.getHours()) + ':' + z(d.getMinutes()) + ':' + z(d.getSeconds())
}
function bareP2pTruncate(s, cols) {
const t = String(s || '')
if (t.length <= cols) return t
return t.slice(0, Math.max(0, cols - 1)) + '\u2026'
}
/**
* Read-only TUI shell: q quits, r refreshes.
* @param {Record<string, unknown>} ctx
* @param {{ title: string, subtitle?: string, renderLines: () => string[] | Promise<string[]>, onRefresh?: () => void | Promise<void> }} opts
*/
async function bareP2pRunSimpleTui(ctx, opts) {
const stdin = ctx.replStdin
const stdout = bareEditResolveStdout(ctx)
if (!stdin || !stdout) {
ctx.console.error('p2p tui: missing stdin/stdout')
ctx.exitCode = 1
return
}
const useColor = bareEditUseColor(ctx)
const envEarly =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
const cols0 = parseInt(envEarly.COLUMNS || '80', 10) || 80
const rows0 = parseInt(envEarly.LINES || '24', 10) || 24
const dims = () => ({
cols:
Math.max(48, /** @type {{ columns?: number }} */ (stdout).columns || cols0),
rows: Math.max(12, /** @type {{ rows?: number }} */ (stdout).rows || rows0)
})
const reader = bareEditCreateStdinReader(stdin)
let suspended = false
let alt = false
let loop = true
const draw = async () => {
const { cols, rows } = dims()
const lines = await opts.renderLines()
let out = '\x1b[?25l\x1b[2J\x1b[H'
const top =
bareEditSgr('status', useColor) +
bareP2pTruncate(' ' + opts.title + ' ', cols) +
EDIT_ANSI_RESET
out += bareEditCup(1, 1) + '\x1b[K' + top
const sub = bareP2pTruncate(
(opts.subtitle || 'q quit · r refresh') + ' · ' + bareP2pFmtClock(Date.now()),
cols
)
out +=
bareEditCup(2, 1) +
'\x1b[K' +
bareEditSgr('dim', useColor) +
sub +
EDIT_ANSI_RESET
out += bareEditCup(3, 1) + '\x1b[K'
const maxBody = Math.max(1, rows - 3)
for (let i = 0; i < maxBody; i++) {
const raw = i < lines.length ? String(lines[i]) : ''
out += bareEditCup(4 + i, 1) + '\x1b[K' + bareP2pTruncate(raw, cols)
}
bareEditWrite(ctx, stdout, out + '\x1b[?25h')
}
try {
if (typeof ctx.suspendReplForSubprocess === 'function') {
ctx.suspendReplForSubprocess()
suspended = true
}
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true)
if (typeof stdin.resume === 'function') stdin.resume()
bareEditWrite(ctx, stdout, '\x1b[?1049h')
alt = true
await draw()
while (loop) {
const ev = await bareEditReadKey(reader)
if (ev.type === 'eof') break
if (ev.type === 'ctrl') {
const code = typeof ev.code === 'number' ? ev.code : 0
if (ev.code === 'interrupt' || code === 3 || code === 17 || code === 24) {
break
}
}
if (ev.type === 'key' && ev.ch) {
if (ev.ch === 'q' || ev.ch === 'Q') break
if (ev.ch === 'r' || ev.ch === 'R') {
if (typeof opts.onRefresh === 'function') await opts.onRefresh()
await draw()
continue
}
}
await draw()
}
} finally {
try {
if (alt) bareEditWrite(ctx, stdout, '\x1b[?1049l')
bareEditWrite(ctx, stdout, '\x1b[?25h\x1b[0m')
} catch {
/* ignore */
}
reader.dispose()
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 argv0 = argv[0] || 'holepunch-view'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' +
argv0 +
'\n' +
'NAT and holepunch summary dashboard.\n' +
'See man holepunch-view.'
)
return
}
const stdin = ctx.replStdin
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
if (!isTTY) {
const h = await bareP2pReadProcJson(ctx, '/proc/bare_os/holepunch_summary.json')
ctx.console.log(JSON.stringify(h, null, 2))
return
}
await bareP2pRunSimpleTui(ctx, {
title: 'holepunch-view',
subtitle: 'nat and holepunch telemetry · q quit · r refresh',
renderLines: async () => {
const h = await bareP2pReadProcJson(ctx, '/proc/bare_os/holepunch_summary.json')
return [
'peers: ' + String(h.peers ?? '?'),
'firewalled: ' + String(h.firewalled ?? '?'),
'randomized: ' + String(h.randomized ?? '?'),
'',
'note: ' + String(h.note || '')
]
}
})
}
@@ -297,10 +297,37 @@ async function bareP2pReadSwarmSnapshot(ctx) {
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
out.peers = peers.slice(0, 128)
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'hypershell-board'
const args = argv.slice(1)
+28 -1
View File
@@ -297,10 +297,37 @@ async function bareP2pReadSwarmSnapshot(ctx) {
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
out.peers = peers.slice(0, 128)
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
const BARE_MESHDROP_CHUNK_RAW = 12 * 1024
function bareMeshdropName(path) {
+366
View File
@@ -0,0 +1,366 @@
/* 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
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'p2ping'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' +
argv0 +
' [LABEL]\n' +
'Emit a p2p ping event and print local swarm timing hints.\n' +
'See man p2ping.'
)
return
}
const label = args.join(' ').trim() || 'ping'
const t0 = Date.now()
const swarm = await bareP2pReadSwarmSnapshot(ctx)
const evtId = bareP2pId('p2ping')
const send = bareP2pSend(ctx, 'p2ping', 'ping', {
evtId,
label,
sentAtMs: t0
})
const t1 = Date.now()
const out = {
schema: 1,
tool: argv0,
evtId,
label,
ok: !(send && send.ok === false),
sendReason: send && send.ok === false ? String(send.reason || 'send_failed') : null,
localElapsedMs: t1 - t0,
swarmPeers: swarm.peerCount
}
ctx.console.log(JSON.stringify(out, null, 2))
if (send && send.ok === false) ctx.exitCode = 1
}
+375
View File
@@ -0,0 +1,375 @@
/* 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
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'p2ptrace'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' +
argv0 +
' [APP] [N]\n' +
'Dump recent p2p events as JSON lines.\n' +
'APP defaults to all known p2p apps.\n' +
'See man p2ptrace.'
)
return
}
const app = String(args[0] || '').trim()
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '200', 10) || 200))
const apps = app
? [app]
: [
'swarmtop',
'meshdrop',
'taskmesh',
'peernote',
'hypershell-board',
'p2ping',
'peerdiscover',
'peerctl'
]
const rows = []
for (const a of apps) {
rows.push(...bareP2pCollectFromHistory(ctx, a, n))
}
rows.sort((a, b) => (a.receivedAtMs || 0) - (b.receivedAtMs || 0))
for (const r of rows.slice(-n)) {
ctx.console.log(
JSON.stringify({
atMs: r.receivedAtMs || 0,
app: r.packet?.app || '?',
kind: r.packet?.kind || '?',
from: r.displayName || r.fromPeerKey || '',
payload: r.packet?.payload || {}
})
)
}
}
+411
View File
@@ -0,0 +1,411 @@
/* 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
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'peerctl'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
ctx.console.log(
'usage: ' +
argv0 +
' health\n' +
' ' +
argv0 +
' request ACTION [JSON_PAYLOAD]\n' +
' ' +
argv0 +
' history [N]\n' +
'P2P control plane helper over event envelopes.\n' +
'See man peerctl.'
)
if (args.length === 0) ctx.exitCode = 1
return
}
const sub = args[0]
if (sub === 'health') {
const swarm = await bareP2pReadSwarmSnapshot(ctx)
const doctor = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
ctx.console.log(
JSON.stringify(
{
schema: 1,
swarmPeers: swarm.peerCount,
health: doctor.health || 'unknown',
remediation: doctor.remediation || []
},
null,
2
)
)
return
}
if (sub === 'request') {
const action = String(args[1] || '').trim()
if (!action) {
ctx.console.error(argv0 + ': request requires ACTION')
ctx.exitCode = 1
return
}
let payload = {}
if (args[2]) {
try {
payload = JSON.parse(args.slice(2).join(' '))
} catch {
ctx.console.error(argv0 + ': invalid JSON payload')
ctx.exitCode = 1
return
}
}
const r = bareP2pSend(ctx, 'peerctl', 'request', {
requestId: bareP2pId('ctl'),
action,
payload
})
if (r && r.ok === false) ctx.exitCode = 1
return
}
if (sub === 'history') {
const n = Math.max(1, Math.min(1000, parseInt(args[1] || '50', 10) || 50))
const rows = bareP2pCollectFromHistory(ctx, 'peerctl', 2000).slice(-n)
for (const row of rows) {
ctx.console.log(
'[' +
bareP2pFmtClock(row.receivedAtMs) +
'] ' +
String(row.packet?.kind || '?') +
' ' +
JSON.stringify(row.packet?.payload || {})
)
}
return
}
ctx.console.error(argv0 + ': unsupported subcommand')
ctx.exitCode = 1
}
@@ -0,0 +1,384 @@
/* 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
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'peerdiscover'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
ctx.console.log(
'usage: ' +
argv0 +
' announce SERVICE [META]\n' +
' ' +
argv0 +
' list [N]\n' +
'Peer service announce/list over p2p event history.\n' +
'See man peerdiscover.'
)
if (args.length === 0) ctx.exitCode = 1
return
}
const sub = args[0]
if (sub === 'announce') {
const service = String(args[1] || '').trim()
if (!service) {
ctx.console.error(argv0 + ': announce requires SERVICE')
ctx.exitCode = 1
return
}
const meta = args.slice(2).join(' ').trim()
const r = bareP2pSend(ctx, 'peerdiscover', 'service.announce', {
service,
meta,
id: bareP2pId('svc')
})
if (r && r.ok === false) ctx.exitCode = 1
return
}
if (sub === 'list') {
const n = Math.max(1, Math.min(500, parseInt(args[1] || '50', 10) || 50))
const rows = bareP2pCollectFromHistory(ctx, 'peerdiscover', 2000)
.filter((r) => r.packet?.kind === 'service.announce')
.slice(-n)
for (const row of rows) {
const p = row.packet?.payload || {}
ctx.console.log(
'[' +
bareP2pFmtClock(row.receivedAtMs) +
'] ' +
String(p.service || 'unknown') +
(p.meta ? ' ' + String(p.meta) : '')
)
}
return
}
ctx.console.error(argv0 + ': unsupported subcommand')
ctx.exitCode = 1
}
+28 -1
View File
@@ -297,10 +297,37 @@ async function bareP2pReadSwarmSnapshot(ctx) {
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
out.peers = peers.slice(0, 128)
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'peernote'
const args = argv.slice(1)
@@ -0,0 +1,951 @@
/* 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'
}
/**
* @param {Record<string, unknown>} ctx
* @param {import('stream').Writable} stdout
* @param {string} s
*/
function bareEditWrite(ctx, stdout, s) {
if (!stdout || typeof stdout.write !== 'function') return
try {
stdout.write(s)
} catch {
try {
ctx.console?.error?.('edit: stdout write failed')
} catch {
/* ignore */
}
}
}
/** 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' }
const tildeNum = /^(\d+)~$/.exec(seq)
if (tildeNum) {
const n = parseInt(tildeNum[1], 10)
const fnMap = {
11: 1,
12: 2,
13: 3,
14: 4,
15: 5,
17: 6,
18: 7,
19: 8,
20: 9,
21: 10,
23: 11,
24: 12
}
if (fnMap[n] != null) return { type: 'fn', n: fnMap[n] }
}
return { type: 'unknown' }
}
if (last === 'Z') return { type: 'nav', key: 'stab' }
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' }
if (b3 === 80) return { type: 'fn', n: 1 }
if (b3 === 81) return { type: 'fn', n: 2 }
if (b3 === 82) return { type: 'fn', n: 3 }
if (b3 === 83) return { type: 'fn', n: 4 }
return { type: 'unknown' }
}
/** ESC [ 200 ~ */
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
/** ESC [ 201 ~ */
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
/**
* @param {number[]} q
* @param {number[]} prefix
*/
function bareEditStartsWithBytes(q, prefix) {
if (q.length < prefix.length) return false
for (let i = 0; i < prefix.length; i++) {
if (q[i] !== prefix[i]) return false
}
return true
}
/**
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
* @param {number[]} q
*/
function bareEditCouldBeBracketPastePrefix(q) {
const pre = BARE_EDIT_BRACKET_PASTE_START
const n = Math.min(q.length, pre.length)
for (let i = 0; i < n; i++) {
if (q[i] !== pre[i]) return false
}
return true
}
/**
* @param {number[]} bytes
*/
function bareEditDecodePasteInner(bytes) {
try {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8', { fatal: false }).decode(
new Uint8Array(bytes)
)
}
} catch {
/* fall through */
}
let s = ''
for (const b of bytes) s += String.fromCharCode(b)
return s
}
/**
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
* @param {number[]} q
* @returns {string|undefined}
*/
function bareEditFinalizeBracketedPasteOnEof(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
const inner = q.slice(SL)
q.length = 0
return bareEditDecodePasteInner(inner)
}
return undefined
}
/**
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
* @param {number[]} q mutable queue
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
*/
function bareEditTryConsumeBracketedPaste(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
const EL = BARE_EDIT_BRACKET_PASTE_END.length
if (!q.length) return undefined
if (q.length < SL) {
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
}
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
return undefined
}
for (let i = SL; i <= q.length - EL; i++) {
let ok = true
for (let j = 0; j < EL; j++) {
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
ok = false
break
}
}
if (ok) {
const inner = q.slice(SL, i)
q.splice(0, i + EL)
return bareEditDecodePasteInner(inner)
}
}
return null
}
/**
* @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) }
}
/** Stdin chunk queue + async key reader for edit/chat TUIs (requires edit-key-parse.js first). */
/**
* @param {unknown} chunk
* @returns {number[]}
*/
function bareEditChunkBytes(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
}
/**
* @param {{ nextByte: () => Promise<number|undefined>, dispose?: () => void, _keyq?: number[] }} reader
*/
async function bareEditReadKey(reader) {
reader._keyq = reader._keyq || []
const q = reader._keyq
for (;;) {
const pasteText = bareEditTryConsumeBracketedPaste(q)
if (pasteText !== undefined && pasteText !== null) {
return { type: 'paste', text: pasteText }
}
if (pasteText === null) {
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
return { type: 'key', ch: '\x1b' }
}
const lone = q.shift()
if (lone !== undefined && lone < 0x20) {
return { type: 'ctrl', code: lone }
}
if (lone !== undefined) {
return { type: 'key', ch: String.fromCharCode(lone) }
}
return { type: 'eof' }
}
q.push(b)
continue
}
const ev = bareEditTryConsumeKey(q)
if (ev) return ev
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
return { type: 'key', ch: '\x1b' }
}
const lone = q.shift()
if (lone !== undefined && lone < 0x20) {
return { type: 'ctrl', code: lone }
}
if (lone !== undefined) {
return { type: 'key', ch: String.fromCharCode(lone) }
}
return { type: 'eof' }
}
q.push(b)
}
}
/**
* @param {import('stream').Readable} stdin
*/
function bareEditCreateStdinReader(stdin) {
/** @type {number[]} */
const bytes = []
/** @type {(() => void)[]} */
const waiters = []
function drain() {
while (waiters.length && bytes.length) {
const w = waiters.shift()
if (w) w()
}
}
/** @param {unknown} chunk */
function onData(chunk) {
bytes.push(...bareEditChunkBytes(chunk))
drain()
}
stdin.on('data', onData)
return {
nextByte() {
if (bytes.length) return Promise.resolve(bytes.shift())
return new Promise((resolve) => {
waiters.push(() => resolve(bytes.shift()))
})
},
dispose() {
stdin.removeListener('data', onData)
}
}
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
function bareP2pFmtClock(ms) {
const d = new Date(typeof ms === 'number' ? ms : Date.now())
const z = (n) => (n < 10 ? '0' : '') + n
return z(d.getHours()) + ':' + z(d.getMinutes()) + ':' + z(d.getSeconds())
}
function bareP2pTruncate(s, cols) {
const t = String(s || '')
if (t.length <= cols) return t
return t.slice(0, Math.max(0, cols - 1)) + '\u2026'
}
/**
* Read-only TUI shell: q quits, r refreshes.
* @param {Record<string, unknown>} ctx
* @param {{ title: string, subtitle?: string, renderLines: () => string[] | Promise<string[]>, onRefresh?: () => void | Promise<void> }} opts
*/
async function bareP2pRunSimpleTui(ctx, opts) {
const stdin = ctx.replStdin
const stdout = bareEditResolveStdout(ctx)
if (!stdin || !stdout) {
ctx.console.error('p2p tui: missing stdin/stdout')
ctx.exitCode = 1
return
}
const useColor = bareEditUseColor(ctx)
const envEarly =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
const cols0 = parseInt(envEarly.COLUMNS || '80', 10) || 80
const rows0 = parseInt(envEarly.LINES || '24', 10) || 24
const dims = () => ({
cols:
Math.max(48, /** @type {{ columns?: number }} */ (stdout).columns || cols0),
rows: Math.max(12, /** @type {{ rows?: number }} */ (stdout).rows || rows0)
})
const reader = bareEditCreateStdinReader(stdin)
let suspended = false
let alt = false
let loop = true
const draw = async () => {
const { cols, rows } = dims()
const lines = await opts.renderLines()
let out = '\x1b[?25l\x1b[2J\x1b[H'
const top =
bareEditSgr('status', useColor) +
bareP2pTruncate(' ' + opts.title + ' ', cols) +
EDIT_ANSI_RESET
out += bareEditCup(1, 1) + '\x1b[K' + top
const sub = bareP2pTruncate(
(opts.subtitle || 'q quit · r refresh') + ' · ' + bareP2pFmtClock(Date.now()),
cols
)
out +=
bareEditCup(2, 1) +
'\x1b[K' +
bareEditSgr('dim', useColor) +
sub +
EDIT_ANSI_RESET
out += bareEditCup(3, 1) + '\x1b[K'
const maxBody = Math.max(1, rows - 3)
for (let i = 0; i < maxBody; i++) {
const raw = i < lines.length ? String(lines[i]) : ''
out += bareEditCup(4 + i, 1) + '\x1b[K' + bareP2pTruncate(raw, cols)
}
bareEditWrite(ctx, stdout, out + '\x1b[?25h')
}
try {
if (typeof ctx.suspendReplForSubprocess === 'function') {
ctx.suspendReplForSubprocess()
suspended = true
}
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true)
if (typeof stdin.resume === 'function') stdin.resume()
bareEditWrite(ctx, stdout, '\x1b[?1049h')
alt = true
await draw()
while (loop) {
const ev = await bareEditReadKey(reader)
if (ev.type === 'eof') break
if (ev.type === 'ctrl') {
const code = typeof ev.code === 'number' ? ev.code : 0
if (ev.code === 'interrupt' || code === 3 || code === 17 || code === 24) {
break
}
}
if (ev.type === 'key' && ev.ch) {
if (ev.ch === 'q' || ev.ch === 'Q') break
if (ev.ch === 'r' || ev.ch === 'R') {
if (typeof opts.onRefresh === 'function') await opts.onRefresh()
await draw()
continue
}
}
await draw()
}
} finally {
try {
if (alt) bareEditWrite(ctx, stdout, '\x1b[?1049l')
bareEditWrite(ctx, stdout, '\x1b[?25h\x1b[0m')
} catch {
/* ignore */
}
reader.dispose()
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 argv0 = argv[0] || 'routeview'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' + argv0 + '\n' + 'Route and relay visibility TUI.\n' + 'See man routeview.'
)
return
}
const stdin = ctx.replStdin
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
if (!isTTY) {
const r = await bareP2pReadProcJson(ctx, '/proc/bare_os/route_summary.json')
ctx.console.log(JSON.stringify(r, null, 2))
return
}
await bareP2pRunSimpleTui(ctx, {
title: 'routeview',
subtitle: 'direct vs relay summary · q quit · r refresh',
renderLines: async () => {
const r = await bareP2pReadProcJson(ctx, '/proc/bare_os/route_summary.json')
return [
'peers: ' + String(r.peers ?? '?'),
'directLikely: ' + String(r.directLikely ?? '?'),
'relayLikely: ' + String(r.relayLikely ?? '?'),
'',
'note: ' + String(r.note || '')
]
}
})
}
@@ -0,0 +1,349 @@
/* 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
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'swarmdoctor'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' +
argv0 +
'\n' +
'Print swarm diagnosis from /proc/bare_os/swarm_doctor.json.\n' +
'See man swarmdoctor.'
)
return
}
const doc = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
ctx.console.log(JSON.stringify(doc, null, 2))
if (doc && typeof doc === 'object' && doc.health && doc.health !== 'ok') {
ctx.exitCode = 1
}
}
+959
View File
@@ -0,0 +1,959 @@
/* 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'
}
/**
* @param {Record<string, unknown>} ctx
* @param {import('stream').Writable} stdout
* @param {string} s
*/
function bareEditWrite(ctx, stdout, s) {
if (!stdout || typeof stdout.write !== 'function') return
try {
stdout.write(s)
} catch {
try {
ctx.console?.error?.('edit: stdout write failed')
} catch {
/* ignore */
}
}
}
/** 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' }
const tildeNum = /^(\d+)~$/.exec(seq)
if (tildeNum) {
const n = parseInt(tildeNum[1], 10)
const fnMap = {
11: 1,
12: 2,
13: 3,
14: 4,
15: 5,
17: 6,
18: 7,
19: 8,
20: 9,
21: 10,
23: 11,
24: 12
}
if (fnMap[n] != null) return { type: 'fn', n: fnMap[n] }
}
return { type: 'unknown' }
}
if (last === 'Z') return { type: 'nav', key: 'stab' }
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' }
if (b3 === 80) return { type: 'fn', n: 1 }
if (b3 === 81) return { type: 'fn', n: 2 }
if (b3 === 82) return { type: 'fn', n: 3 }
if (b3 === 83) return { type: 'fn', n: 4 }
return { type: 'unknown' }
}
/** ESC [ 200 ~ */
const BARE_EDIT_BRACKET_PASTE_START = [27, 91, 50, 48, 48, 126]
/** ESC [ 201 ~ */
const BARE_EDIT_BRACKET_PASTE_END = [27, 91, 50, 48, 49, 126]
/**
* @param {number[]} q
* @param {number[]} prefix
*/
function bareEditStartsWithBytes(q, prefix) {
if (q.length < prefix.length) return false
for (let i = 0; i < prefix.length; i++) {
if (q[i] !== prefix[i]) return false
}
return true
}
/**
* True if q could still become BARE_EDIT_BRACKET_PASTE_START with more bytes.
* @param {number[]} q
*/
function bareEditCouldBeBracketPastePrefix(q) {
const pre = BARE_EDIT_BRACKET_PASTE_START
const n = Math.min(q.length, pre.length)
for (let i = 0; i < n; i++) {
if (q[i] !== pre[i]) return false
}
return true
}
/**
* @param {number[]} bytes
*/
function bareEditDecodePasteInner(bytes) {
try {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8', { fatal: false }).decode(
new Uint8Array(bytes)
)
}
} catch {
/* fall through */
}
let s = ''
for (const b of bytes) s += String.fromCharCode(b)
return s
}
/**
* If queue begins with ESC [ 200 ~ but no closing ESC [ 201 ~ (stdin closed), return inner bytes as text.
* @param {number[]} q
* @returns {string|undefined}
*/
function bareEditFinalizeBracketedPasteOnEof(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
if (q.length >= SL && bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
const inner = q.slice(SL)
q.length = 0
return bareEditDecodePasteInner(inner)
}
return undefined
}
/**
* xterm bracketed paste: ESC [ 200 ~ … ESC [ 201 ~
* @param {number[]} q mutable queue
* @returns {string|null|undefined} string if consumed; null if incomplete; undefined if not bracketed paste at front
*/
function bareEditTryConsumeBracketedPaste(q) {
const SL = BARE_EDIT_BRACKET_PASTE_START.length
const EL = BARE_EDIT_BRACKET_PASTE_END.length
if (!q.length) return undefined
if (q.length < SL) {
return bareEditCouldBeBracketPastePrefix(q) ? null : undefined
}
if (!bareEditStartsWithBytes(q, BARE_EDIT_BRACKET_PASTE_START)) {
return undefined
}
for (let i = SL; i <= q.length - EL; i++) {
let ok = true
for (let j = 0; j < EL; j++) {
if (q[i + j] !== BARE_EDIT_BRACKET_PASTE_END[j]) {
ok = false
break
}
}
if (ok) {
const inner = q.slice(SL, i)
q.splice(0, i + EL)
return bareEditDecodePasteInner(inner)
}
}
return null
}
/**
* @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) }
}
/** Stdin chunk queue + async key reader for edit/chat TUIs (requires edit-key-parse.js first). */
/**
* @param {unknown} chunk
* @returns {number[]}
*/
function bareEditChunkBytes(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
}
/**
* @param {{ nextByte: () => Promise<number|undefined>, dispose?: () => void, _keyq?: number[] }} reader
*/
async function bareEditReadKey(reader) {
reader._keyq = reader._keyq || []
const q = reader._keyq
for (;;) {
const pasteText = bareEditTryConsumeBracketedPaste(q)
if (pasteText !== undefined && pasteText !== null) {
return { type: 'paste', text: pasteText }
}
if (pasteText === null) {
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
return { type: 'key', ch: '\x1b' }
}
const lone = q.shift()
if (lone !== undefined && lone < 0x20) {
return { type: 'ctrl', code: lone }
}
if (lone !== undefined) {
return { type: 'key', ch: String.fromCharCode(lone) }
}
return { type: 'eof' }
}
q.push(b)
continue
}
const ev = bareEditTryConsumeKey(q)
if (ev) return ev
const b = await reader.nextByte()
if (b === undefined) {
const eofPaste = bareEditFinalizeBracketedPasteOnEof(q)
if (eofPaste !== undefined) {
return { type: 'paste', text: eofPaste }
}
if (!q.length) return { type: 'eof' }
if (q.length === 1 && q[0] === 27) {
q.length = 0
return { type: 'key', ch: '\x1b' }
}
const lone = q.shift()
if (lone !== undefined && lone < 0x20) {
return { type: 'ctrl', code: lone }
}
if (lone !== undefined) {
return { type: 'key', ch: String.fromCharCode(lone) }
}
return { type: 'eof' }
}
q.push(b)
}
}
/**
* @param {import('stream').Readable} stdin
*/
function bareEditCreateStdinReader(stdin) {
/** @type {number[]} */
const bytes = []
/** @type {(() => void)[]} */
const waiters = []
function drain() {
while (waiters.length && bytes.length) {
const w = waiters.shift()
if (w) w()
}
}
/** @param {unknown} chunk */
function onData(chunk) {
bytes.push(...bareEditChunkBytes(chunk))
drain()
}
stdin.on('data', onData)
return {
nextByte() {
if (bytes.length) return Promise.resolve(bytes.shift())
return new Promise((resolve) => {
waiters.push(() => resolve(bytes.shift()))
})
},
dispose() {
stdin.removeListener('data', onData)
}
}
}
const BARE_P2P_SUITE_PREFIX = '[bare-p2p-v1]'
const BARE_P2P_SUITE_MAX_HISTORY = 2000
function bareP2pNowMs() {
return Date.now()
}
function bareP2pId(prefix) {
const rnd = Math.floor(Math.random() * 0x7fffffff)
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
}
function bareP2pHome(ctx) {
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
return home
}
function bareP2pJoinPath(a, b) {
if (!a.endsWith('/')) return a + '/' + b
return a + b
}
function bareP2pDataDir(ctx) {
return bareP2pJoinPath(bareP2pHome(ctx), '.bare/p2p-suite')
}
function bareP2pDataFile(ctx, app) {
return bareP2pJoinPath(bareP2pDataDir(ctx), app + '.json')
}
async function bareP2pReadJson(ctx, path) {
try {
if (!ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
const b = await ctx.vfs.readFile(path)
if (!b) return null
const t = ctx.b4a.toString(b).trim()
if (!t) return null
return JSON.parse(t)
} catch {
return null
}
}
async function bareP2pWriteJson(ctx, path, obj) {
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
const body = JSON.stringify(obj, null, 2) + '\n'
const b4 = ctx.b4a
const buf =
b4 && typeof b4.from === 'function'
? b4.from(body)
: new TextEncoder().encode(body)
try {
await ctx.vfs.writeFile(path, buf)
return true
} catch {
return false
}
}
function bareP2pDecodeEnvelope(line) {
const s = String(line || '')
if (!s.startsWith(BARE_P2P_SUITE_PREFIX)) return null
const payload = s.slice(BARE_P2P_SUITE_PREFIX.length).trim()
if (!payload) return null
try {
const obj = JSON.parse(payload)
if (!obj || typeof obj !== 'object') return null
return obj
} catch {
return null
}
}
function bareP2pEncodeEnvelope(app, kind, payload) {
return (
BARE_P2P_SUITE_PREFIX +
' ' +
JSON.stringify({
schema: 1,
suite: 'bare-p2p',
version: '1',
app,
kind,
tsMs: bareP2pNowMs(),
payload: payload || {}
})
)
}
function bareP2pSend(ctx, app, kind, payload) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSend === 'function') {
return ctx.bareOsMeshdropSend({
app,
kind,
payload: payload || {},
tsMs: bareP2pNowMs()
})
}
if (typeof ctx.bareOsChatSend !== 'function') {
return { ok: false, reason: 'bareOsChatSend unavailable' }
}
return ctx.bareOsChatSend(bareP2pEncodeEnvelope(app, kind, payload))
}
function bareP2pCollectFromHistory(ctx, app, limit) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropHistory === 'function') {
let hist = []
try {
hist = ctx.bareOsMeshdropHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
if (ev.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
}
return out
}
if (typeof ctx.bareOsChatHistory !== 'function') return []
let hist = []
try {
hist = ctx.bareOsChatHistory(
Math.max(1, Math.min(BARE_P2P_SUITE_MAX_HISTORY, limit || 512))
)
} catch {
hist = []
}
if (!Array.isArray(hist)) return []
const out = []
for (const ev of hist) {
if (!ev || typeof ev !== 'object') continue
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) continue
out.push({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
}
return out
}
function bareP2pSubscribe(ctx, app, fn) {
if (app === 'meshdrop' && typeof ctx.bareOsMeshdropSubscribe === 'function') {
return ctx.bareOsMeshdropSubscribe((ev) => {
if (!ev || typeof ev !== 'object' || ev.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.sender === 'string' ? ev.sender : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: {
app,
kind: typeof ev.kind === 'string' ? ev.kind : 'unknown',
payload: ev.payload && typeof ev.payload === 'object' ? ev.payload : {}
}
})
})
}
if (typeof ctx.bareOsChatSubscribe !== 'function') return () => {}
return ctx.bareOsChatSubscribe((ev) => {
if (!ev || typeof ev !== 'object') return
const body = String(ev.body || '')
const decoded = bareP2pDecodeEnvelope(body)
if (!decoded || decoded.app !== app) return
fn({
fromPeerKey: typeof ev.fromPeerKey === 'string' ? ev.fromPeerKey : '',
displayName: typeof ev.displayName === 'string' ? ev.displayName : '',
local: Boolean(ev.local),
receivedAtMs:
typeof ev.receivedAtMs === 'number' ? ev.receivedAtMs : bareP2pNowMs(),
packet: decoded
})
})
}
async function bareP2pReadSwarmSnapshot(ctx) {
const out = {
atMs: bareP2pNowMs(),
peerCount: null,
topicCount: null,
peers: []
}
const snap = await bareP2pReadJson(ctx, '/proc/bare_os/swarm')
if (!snap || typeof snap !== 'object') return out
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
function bareP2pFmtClock(ms) {
const d = new Date(typeof ms === 'number' ? ms : Date.now())
const z = (n) => (n < 10 ? '0' : '') + n
return z(d.getHours()) + ':' + z(d.getMinutes()) + ':' + z(d.getSeconds())
}
function bareP2pTruncate(s, cols) {
const t = String(s || '')
if (t.length <= cols) return t
return t.slice(0, Math.max(0, cols - 1)) + '\u2026'
}
/**
* Read-only TUI shell: q quits, r refreshes.
* @param {Record<string, unknown>} ctx
* @param {{ title: string, subtitle?: string, renderLines: () => string[] | Promise<string[]>, onRefresh?: () => void | Promise<void> }} opts
*/
async function bareP2pRunSimpleTui(ctx, opts) {
const stdin = ctx.replStdin
const stdout = bareEditResolveStdout(ctx)
if (!stdin || !stdout) {
ctx.console.error('p2p tui: missing stdin/stdout')
ctx.exitCode = 1
return
}
const useColor = bareEditUseColor(ctx)
const envEarly =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
const cols0 = parseInt(envEarly.COLUMNS || '80', 10) || 80
const rows0 = parseInt(envEarly.LINES || '24', 10) || 24
const dims = () => ({
cols:
Math.max(48, /** @type {{ columns?: number }} */ (stdout).columns || cols0),
rows: Math.max(12, /** @type {{ rows?: number }} */ (stdout).rows || rows0)
})
const reader = bareEditCreateStdinReader(stdin)
let suspended = false
let alt = false
let loop = true
const draw = async () => {
const { cols, rows } = dims()
const lines = await opts.renderLines()
let out = '\x1b[?25l\x1b[2J\x1b[H'
const top =
bareEditSgr('status', useColor) +
bareP2pTruncate(' ' + opts.title + ' ', cols) +
EDIT_ANSI_RESET
out += bareEditCup(1, 1) + '\x1b[K' + top
const sub = bareP2pTruncate(
(opts.subtitle || 'q quit · r refresh') + ' · ' + bareP2pFmtClock(Date.now()),
cols
)
out +=
bareEditCup(2, 1) +
'\x1b[K' +
bareEditSgr('dim', useColor) +
sub +
EDIT_ANSI_RESET
out += bareEditCup(3, 1) + '\x1b[K'
const maxBody = Math.max(1, rows - 3)
for (let i = 0; i < maxBody; i++) {
const raw = i < lines.length ? String(lines[i]) : ''
out += bareEditCup(4 + i, 1) + '\x1b[K' + bareP2pTruncate(raw, cols)
}
bareEditWrite(ctx, stdout, out + '\x1b[?25h')
}
try {
if (typeof ctx.suspendReplForSubprocess === 'function') {
ctx.suspendReplForSubprocess()
suspended = true
}
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true)
if (typeof stdin.resume === 'function') stdin.resume()
bareEditWrite(ctx, stdout, '\x1b[?1049h')
alt = true
await draw()
while (loop) {
const ev = await bareEditReadKey(reader)
if (ev.type === 'eof') break
if (ev.type === 'ctrl') {
const code = typeof ev.code === 'number' ? ev.code : 0
if (ev.code === 'interrupt' || code === 3 || code === 17 || code === 24) {
break
}
}
if (ev.type === 'key' && ev.ch) {
if (ev.ch === 'q' || ev.ch === 'Q') break
if (ev.ch === 'r' || ev.ch === 'R') {
if (typeof opts.onRefresh === 'function') await opts.onRefresh()
await draw()
continue
}
}
await draw()
}
} finally {
try {
if (alt) bareEditWrite(ctx, stdout, '\x1b[?1049l')
bareEditWrite(ctx, stdout, '\x1b[?25h\x1b[0m')
} catch {
/* ignore */
}
reader.dispose()
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 argv0 = argv[0] || 'swarmmap'
const args = argv.slice(1)
if (args.includes('-h') || args.includes('--help')) {
ctx.console.log(
'usage: ' + argv0 + '\n' + 'Swarm topology map TUI.\n' + 'See man swarmmap.'
)
return
}
const stdin = ctx.replStdin
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
const snap = await bareP2pReadSwarmSnapshot(ctx)
if (!isTTY) {
ctx.console.log(JSON.stringify(snap, null, 2))
return
}
await bareP2pRunSimpleTui(ctx, {
title: 'swarmmap',
subtitle: 'peer topology · q quit · r refresh',
renderLines: async () => {
const s = await bareP2pReadSwarmSnapshot(ctx)
const lines = []
lines.push('peerCount: ' + String(s.peerCount ?? '?'))
lines.push('topicCount: ' + String(s.topicCount ?? '?'))
lines.push('')
lines.push('Peers:')
const peers = Array.isArray(s.peers) ? s.peers : []
if (!peers.length) lines.push(' (none)')
for (const p of peers.slice(0, 48)) {
const id =
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
? p.remotePublicKey.slice(0, 20)
: '?'
lines.push(' ' + id + ' ' + String(p.state || 'connected'))
}
return lines
}
})
}
+46 -4
View File
@@ -768,10 +768,37 @@ async function bareP2pReadSwarmSnapshot(ctx) {
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
out.peers = peers.slice(0, 128)
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
function bareP2pFmtClock(ms) {
const d = new Date(typeof ms === 'number' ? ms : Date.now())
const z = (n) => (n < 10 ? '0' : '') + n
@@ -978,16 +1005,31 @@ async function run(ctx, argv) {
lines.push('Peers:')
const peers = Array.isArray(snap.peers) ? snap.peers : []
if (!peers.length) lines.push(' (no peers reported)')
for (const p of peers.slice(0, 24)) {
for (const p of peers.slice(0, 12)) {
const key =
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
? p.remotePublicKey.slice(0, 16)
? p.remotePublicKey
: '?'
const state =
p && typeof p === 'object' && typeof p.state === 'string'
? p.state
: 'connected'
lines.push(' ' + key + ' ' + state)
const addr =
p && typeof p === 'object' && p.endpoint && typeof p.endpoint === 'object'
? String(p.endpoint.address || p.remoteAddress || '?') +
':' +
String(p.endpoint.port || p.remotePort || '?')
: String(p?.remoteAddress || '?') + ':' + String(p?.remotePort || '?')
const osHint =
p &&
typeof p === 'object' &&
p.peerOs &&
typeof p.peerOs === 'object' &&
p.peerOs.osHint
? String(p.peerOs.osHint)
: 'unknown'
lines.push(' pubkey: ' + key)
lines.push(' state=' + state + ' endpoint=' + addr + ' os=' + osHint)
}
lines.push('')
lines.push('Recent events:')
+28 -1
View File
@@ -297,10 +297,37 @@ async function bareP2pReadSwarmSnapshot(ctx) {
if (typeof snap.peerCount === 'number') out.peerCount = snap.peerCount
if (typeof snap.topicCount === 'number') out.topicCount = snap.topicCount
const peers = Array.isArray(snap.peers) ? snap.peers : []
out.peers = peers.slice(0, 128)
const detailsRaw = await bareP2pReadJson(ctx, '/proc/bare_os/peer_details.json')
const details =
detailsRaw && typeof detailsRaw === 'object' && Array.isArray(detailsRaw.peers)
? detailsRaw.peers
: []
/** @type {Map<string, Record<string, unknown>>} */
const byKey = new Map()
for (const d of details) {
if (!d || typeof d !== 'object') continue
const rk = typeof d.remotePublicKey === 'string' ? d.remotePublicKey : ''
const pid = typeof d.peerId === 'string' ? d.peerId : ''
if (rk) byKey.set('k:' + rk, d)
if (pid) byKey.set('p:' + pid, d)
}
const merged = []
for (const p of peers) {
if (!p || typeof p !== 'object') continue
const rk = typeof p.remotePublicKey === 'string' ? p.remotePublicKey : ''
const pid = typeof p.peerId === 'string' ? p.peerId : ''
const d = (rk && byKey.get('k:' + rk)) || (pid && byKey.get('p:' + pid)) || null
merged.push(d && typeof d === 'object' ? { ...p, ...d } : p)
}
out.peers = merged.slice(0, 128)
return out
}
async function bareP2pReadProcJson(ctx, path) {
const v = await bareP2pReadJson(ctx, path)
return v && typeof v === 'object' ? v : {}
}
async function run(ctx, argv) {
const argv0 = argv[0] || 'taskmesh'
const args = argv.slice(1)
@@ -1,7 +1,7 @@
{
"schema": 2,
"profileId": "bare-os-posix-like",
"generatedAt": "2026-04-26T11:47:34.024Z",
"generatedAt": "2026-04-26T12:01:40.794Z",
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
"commandIndex": [
{
@@ -108,6 +108,14 @@
"name": "df",
"tier": "tier1_bin"
},
{
"name": "dhtscan",
"tier": "tier1_bin"
},
{
"name": "dhttop",
"tier": "tier1_bin"
},
{
"name": "diff",
"tier": "tier1_bin"
@@ -204,6 +212,10 @@
"name": "help",
"tier": "tier1_bin"
},
{
"name": "holepunch-view",
"tier": "tier1_bin"
},
{
"name": "holesail",
"tier": "tier1_bin"
@@ -372,6 +384,14 @@
"name": "openssl",
"tier": "tier1_bin"
},
{
"name": "p2ping",
"tier": "tier1_bin"
},
{
"name": "p2ptrace",
"tier": "tier1_bin"
},
{
"name": "paste",
"tier": "tier1_bin"
@@ -388,6 +408,14 @@
"name": "pathchk",
"tier": "tier1_bin"
},
{
"name": "peerctl",
"tier": "tier1_bin"
},
{
"name": "peerdiscover",
"tier": "tier1_bin"
},
{
"name": "peernote",
"tier": "tier1_bin"
@@ -440,6 +468,10 @@
"name": "rmdir",
"tier": "tier1_bin"
},
{
"name": "routeview",
"tier": "tier1_bin"
},
{
"name": "savevault",
"tier": "tier1_bin"
@@ -516,6 +548,14 @@
"name": "sum",
"tier": "tier1_bin"
},
{
"name": "swarmdoctor",
"tier": "tier1_bin"
},
{
"name": "swarmmap",
"tier": "tier1_bin"
},
{
"name": "swarmtop",
"tier": "tier1_bin"
@@ -1,6 +1,6 @@
{
"schema": 1,
"atMs": 1777204054023,
"atMs": 1777204900794,
"commands": [
"agent",
"arch",
@@ -28,6 +28,8 @@
"date",
"dd",
"df",
"dhtscan",
"dhttop",
"diff",
"dir",
"dircolors",
@@ -52,6 +54,7 @@
"hdms",
"head",
"help",
"holepunch-view",
"holesail",
"hostid",
"hostname",
@@ -94,10 +97,14 @@
"od",
"oidc-publish",
"openssl",
"p2ping",
"p2ptrace",
"paste",
"patch",
"pathcap-verify",
"pathchk",
"peerctl",
"peerdiscover",
"peernote",
"pkg-swarm-index",
"pr",
@@ -111,6 +118,7 @@
"rev",
"rm",
"rmdir",
"routeview",
"savevault",
"say",
"sed",
@@ -130,6 +138,8 @@
"sshd",
"stat",
"sum",
"swarmdoctor",
"swarmmap",
"swarmtop",
"sync",
"tac",
File diff suppressed because one or more lines are too long