feat(p2p): introduce shared p2p transport/service stack and launch swarmtop, meshdrop, taskmesh, peernote, and hypershell-board
This commit is contained in:
+1
-1
@@ -87,7 +87,7 @@ function bareOsEmitRaw(ctx, chunk) {
|
|||||||
return false
|
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 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 mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl paste patch pathcap-verify pathchk pear-runtime-matrix 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 sync systemctl tac tail tar tar 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 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"
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
|
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
|
||||||
|
|||||||
@@ -0,0 +1,406 @@
|
|||||||
|
/* 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 : []
|
||||||
|
out.peers = peers.slice(0, 128)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'hypershell-board'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' offer-shell LABEL\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' offer-copy PATH\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' claim SESSION_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' close SESSION_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' list\n' +
|
||||||
|
'P2P hypershell-style session board (intent + audit feed).\n' +
|
||||||
|
'See man hypershell-board.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'offer-shell' || sub === 'offer-copy') {
|
||||||
|
const label = args.slice(1).join(' ').trim()
|
||||||
|
if (!label) {
|
||||||
|
ctx.console.error(argv0 + ': missing label/path')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sessionId = bareP2pId('session')
|
||||||
|
const mode = sub === 'offer-shell' ? 'shell' : 'copy'
|
||||||
|
const r = bareP2pSend(ctx, 'hypershell-board', 'session.offer', {
|
||||||
|
sessionId,
|
||||||
|
mode,
|
||||||
|
label
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log('offered ' + sessionId + ' (' + mode + ')')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'claim' || sub === 'close') {
|
||||||
|
const sessionId = String(args[1] || '').trim()
|
||||||
|
if (!sessionId) {
|
||||||
|
ctx.console.error(argv0 + ': missing SESSION_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const kind = sub === 'claim' ? 'session.claim' : 'session.close'
|
||||||
|
const r = bareP2pSend(ctx, 'hypershell-board', kind, { sessionId })
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log(sub + ' ' + sessionId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'list') {
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'hypershell-board', 2000)
|
||||||
|
/** @type {Map<string, { sessionId: string, mode: string, label: string, state: string, atMs: number }>} */
|
||||||
|
const sessions = new Map()
|
||||||
|
for (const row of rows) {
|
||||||
|
const kind = row.packet?.kind
|
||||||
|
const p = row.packet?.payload
|
||||||
|
if (!p || typeof p !== 'object') continue
|
||||||
|
if (kind === 'session.offer' && typeof p.sessionId === 'string') {
|
||||||
|
sessions.set(p.sessionId, {
|
||||||
|
sessionId: p.sessionId,
|
||||||
|
mode: typeof p.mode === 'string' ? p.mode : 'shell',
|
||||||
|
label: typeof p.label === 'string' ? p.label : '',
|
||||||
|
state: 'open',
|
||||||
|
atMs: row.receivedAtMs
|
||||||
|
})
|
||||||
|
} else if (kind === 'session.claim' && typeof p.sessionId === 'string') {
|
||||||
|
const s = sessions.get(p.sessionId)
|
||||||
|
if (s) s.state = 'claimed'
|
||||||
|
} else if (kind === 'session.close' && typeof p.sessionId === 'string') {
|
||||||
|
const s = sessions.get(p.sessionId)
|
||||||
|
if (s) s.state = 'closed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const s of [...sessions.values()].sort((a, b) => b.atMs - a.atMs)) {
|
||||||
|
ctx.console.log(
|
||||||
|
'[' +
|
||||||
|
s.state +
|
||||||
|
'] ' +
|
||||||
|
s.sessionId +
|
||||||
|
' ' +
|
||||||
|
s.mode +
|
||||||
|
' ' +
|
||||||
|
s.label
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,766 @@
|
|||||||
|
/* 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 : []
|
||||||
|
out.peers = peers.slice(0, 128)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const BARE_MESHDROP_CHUNK_RAW = 12 * 1024
|
||||||
|
|
||||||
|
function bareMeshdropName(path) {
|
||||||
|
const parts = String(path || '').split('/')
|
||||||
|
return parts[parts.length - 1] || 'file.bin'
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareMeshdropChunkB64Limit() {
|
||||||
|
const raw = BARE_MESHDROP_CHUNK_RAW
|
||||||
|
return Math.ceil((raw * 4) / 3) + 16
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareMeshdropChunkSliceB64(b64, index) {
|
||||||
|
const max = bareMeshdropChunkB64Limit()
|
||||||
|
const from = index * max
|
||||||
|
const to = Math.min(b64.length, from + max)
|
||||||
|
return b64.slice(from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bareMeshdropDbRead(ctx) {
|
||||||
|
const p = bareP2pDataFile(ctx, 'meshdrop')
|
||||||
|
const cur = await bareP2pReadJson(ctx, p)
|
||||||
|
if (cur && typeof cur === 'object') return cur
|
||||||
|
return {
|
||||||
|
schema: 2,
|
||||||
|
outgoing: {},
|
||||||
|
incoming: {},
|
||||||
|
transfers: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bareMeshdropDbWrite(ctx, db) {
|
||||||
|
return bareP2pWriteJson(ctx, bareP2pDataFile(ctx, 'meshdrop'), db)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareMeshdropEnsureTransfer(db, transferId) {
|
||||||
|
if (!db.transfers || typeof db.transfers !== 'object') db.transfers = {}
|
||||||
|
if (!db.transfers[transferId]) {
|
||||||
|
db.transfers[transferId] = {
|
||||||
|
transferId,
|
||||||
|
status: 'new',
|
||||||
|
chunksTotal: 0,
|
||||||
|
chunksReceived: 0,
|
||||||
|
updatedAtMs: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db.transfers[transferId]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'meshdrop'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' offer FILE [--to PEER_HINT]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' send-next OFFER_ID [MAX_CHUNKS]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' inbox [N]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' accept OFFER_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' fetch OFFER_ID [DEST]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' status [TRANSFER_ID]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' cancel TRANSFER_ID\n' +
|
||||||
|
'P2P file inbox/outbox over bare-p2p envelopes on chat transport.\n' +
|
||||||
|
'See man meshdrop.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'offer') {
|
||||||
|
const file = args[1]
|
||||||
|
if (!file) {
|
||||||
|
ctx.console.error(argv0 + ': offer requires FILE')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const b = await ctx.vfs.readFile(file)
|
||||||
|
if (!b) {
|
||||||
|
ctx.console.error(argv0 + ': unable to read ' + file)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const b64 = ctx.b4a.toString(b, 'base64')
|
||||||
|
const toIdx = args.indexOf('--to')
|
||||||
|
const to = toIdx >= 0 ? String(args[toIdx + 1] || '').trim() : ''
|
||||||
|
const offerId = bareP2pId('offer')
|
||||||
|
const transferId = bareP2pId('xfer')
|
||||||
|
const chunkChars = bareMeshdropChunkB64Limit()
|
||||||
|
const chunksTotal = Math.max(1, Math.ceil(b64.length / chunkChars))
|
||||||
|
const firstChunk = bareMeshdropChunkSliceB64(b64, 0)
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
if (!db.outgoing || typeof db.outgoing !== 'object') db.outgoing = {}
|
||||||
|
db.outgoing[offerId] = {
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
file,
|
||||||
|
fileName: bareMeshdropName(file),
|
||||||
|
byteLength: b.byteLength,
|
||||||
|
base64: b64,
|
||||||
|
chunkChars,
|
||||||
|
chunksTotal,
|
||||||
|
sentChunks: firstChunk ? 1 : 0,
|
||||||
|
to,
|
||||||
|
status: 'offered',
|
||||||
|
updatedAtMs: Date.now()
|
||||||
|
}
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, transferId)
|
||||||
|
tr.role = 'sender'
|
||||||
|
tr.offerId = offerId
|
||||||
|
tr.status = 'offered'
|
||||||
|
tr.chunksTotal = chunksTotal
|
||||||
|
tr.chunksReceived = 0
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
|
||||||
|
if (typeof ctx.bareOsEmitMirrorDriveHint === 'function') {
|
||||||
|
ctx.bareOsEmitMirrorDriveHint({
|
||||||
|
app: 'meshdrop',
|
||||||
|
phase: 'offer',
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
fileName: bareMeshdropName(file),
|
||||||
|
byteLength: b.byteLength,
|
||||||
|
chunksTotal
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const r = bareP2pSend(ctx, 'meshdrop', 'offer', {
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
fromPath: file,
|
||||||
|
fileName: bareMeshdropName(file),
|
||||||
|
byteLength: b.byteLength,
|
||||||
|
chunkChars,
|
||||||
|
chunksTotal,
|
||||||
|
firstChunkB64: firstChunk,
|
||||||
|
to
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) {
|
||||||
|
ctx.console.error(argv0 + ': ' + String(r.reason || 'send failed'))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(
|
||||||
|
'offered ' + file + ' as ' + offerId + ' (' + chunksTotal + ' chunks)'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'send-next') {
|
||||||
|
const offerId = String(args[1] || '').trim()
|
||||||
|
if (!offerId) {
|
||||||
|
ctx.console.error(argv0 + ': send-next requires OFFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const maxChunks = Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(64, parseInt(args[2] || '8', 10) || 8)
|
||||||
|
)
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const rec = db?.outgoing?.[offerId]
|
||||||
|
if (!rec) {
|
||||||
|
ctx.console.error(argv0 + ': unknown offer: ' + offerId)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let sent = 0
|
||||||
|
while (sent < maxChunks && rec.sentChunks < rec.chunksTotal) {
|
||||||
|
const idx = rec.sentChunks
|
||||||
|
const chunk = bareMeshdropChunkSliceB64(rec.base64, idx)
|
||||||
|
const r = bareP2pSend(ctx, 'meshdrop', 'chunk', {
|
||||||
|
offerId: rec.offerId,
|
||||||
|
transferId: rec.transferId,
|
||||||
|
index: idx,
|
||||||
|
chunksTotal: rec.chunksTotal,
|
||||||
|
chunkB64: chunk
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) break
|
||||||
|
rec.sentChunks++
|
||||||
|
rec.updatedAtMs = Date.now()
|
||||||
|
sent++
|
||||||
|
}
|
||||||
|
rec.status = rec.sentChunks >= rec.chunksTotal ? 'all-chunks-sent' : 'sending'
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, rec.transferId)
|
||||||
|
tr.status = rec.status
|
||||||
|
tr.updatedAtMs = rec.updatedAtMs
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'progress', {
|
||||||
|
offerId: rec.offerId,
|
||||||
|
transferId: rec.transferId,
|
||||||
|
sentChunks: rec.sentChunks,
|
||||||
|
chunksTotal: rec.chunksTotal
|
||||||
|
})
|
||||||
|
ctx.console.log(
|
||||||
|
'sent ' + sent + ' chunk(s), ' + rec.sentChunks + '/' + rec.chunksTotal
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'inbox') {
|
||||||
|
const n = Math.max(1, Math.min(200, parseInt(args[1] || '20', 10) || 20))
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
const offers = rows.filter((r) => r.packet?.kind === 'offer').slice(-n)
|
||||||
|
for (const row of offers) {
|
||||||
|
const p = row.packet?.payload || {}
|
||||||
|
const offerId = typeof p.offerId === 'string' ? p.offerId : '?'
|
||||||
|
const fileName = typeof p.fileName === 'string' ? p.fileName : '?'
|
||||||
|
const bytes = typeof p.byteLength === 'number' ? p.byteLength : 0
|
||||||
|
const chunks = typeof p.chunksTotal === 'number' ? p.chunksTotal : '?'
|
||||||
|
const from = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
|
ctx.console.log(
|
||||||
|
'[' +
|
||||||
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
|
'] ' +
|
||||||
|
offerId +
|
||||||
|
' ' +
|
||||||
|
fileName +
|
||||||
|
' ' +
|
||||||
|
bytes +
|
||||||
|
'B chunks=' +
|
||||||
|
String(chunks) +
|
||||||
|
' from=' +
|
||||||
|
from
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'accept') {
|
||||||
|
const offerId = String(args[1] || '').trim()
|
||||||
|
if (!offerId) {
|
||||||
|
ctx.console.error(argv0 + ': accept requires OFFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
const offer = rows.findLast(
|
||||||
|
(r) =>
|
||||||
|
r.packet?.kind === 'offer' &&
|
||||||
|
r.packet.payload &&
|
||||||
|
typeof r.packet.payload === 'object' &&
|
||||||
|
r.packet.payload.offerId === offerId
|
||||||
|
)
|
||||||
|
if (!offer) {
|
||||||
|
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const p = offer.packet.payload
|
||||||
|
const transferId =
|
||||||
|
typeof p.transferId === 'string' ? p.transferId : bareP2pId('xfer')
|
||||||
|
const fileName =
|
||||||
|
typeof p.fileName === 'string' ? p.fileName : offerId + '.received'
|
||||||
|
const chunksTotal = typeof p.chunksTotal === 'number' ? p.chunksTotal : 1
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
if (!db.incoming || typeof db.incoming !== 'object') db.incoming = {}
|
||||||
|
db.incoming[offerId] = {
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
fileName,
|
||||||
|
chunksTotal,
|
||||||
|
chunks: {},
|
||||||
|
acceptedAtMs: Date.now(),
|
||||||
|
status: 'accepted'
|
||||||
|
}
|
||||||
|
if (typeof p.firstChunkB64 === 'string' && p.firstChunkB64) {
|
||||||
|
db.incoming[offerId].chunks[0] = p.firstChunkB64
|
||||||
|
}
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, transferId)
|
||||||
|
tr.role = 'receiver'
|
||||||
|
tr.offerId = offerId
|
||||||
|
tr.status = 'accepted'
|
||||||
|
tr.chunksTotal = chunksTotal
|
||||||
|
tr.chunksReceived = Object.keys(db.incoming[offerId].chunks).length
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'accept', { offerId, transferId })
|
||||||
|
ctx.console.log('accepted ' + offerId + ' (transfer ' + transferId + ')')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'fetch') {
|
||||||
|
const offerId = String(args[1] || '').trim()
|
||||||
|
if (!offerId) {
|
||||||
|
ctx.console.error(argv0 + ': fetch requires OFFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
const offer = rows.findLast(
|
||||||
|
(r) =>
|
||||||
|
r.packet?.kind === 'offer' &&
|
||||||
|
r.packet.payload &&
|
||||||
|
typeof r.packet.payload === 'object' &&
|
||||||
|
r.packet.payload.offerId === offerId
|
||||||
|
)
|
||||||
|
if (!offer) {
|
||||||
|
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const p = offer.packet.payload
|
||||||
|
const fileName =
|
||||||
|
typeof p.fileName === 'string' ? p.fileName : offerId + '.bin'
|
||||||
|
const dest = args[2] || fileName
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const incoming = db?.incoming?.[offerId]
|
||||||
|
if (!incoming) {
|
||||||
|
ctx.console.error(
|
||||||
|
argv0 +
|
||||||
|
': offer is not accepted locally; run `' +
|
||||||
|
argv0 +
|
||||||
|
' accept ' +
|
||||||
|
offerId +
|
||||||
|
'` first'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const chunks = incoming.chunks && typeof incoming.chunks === 'object' ? incoming.chunks : {}
|
||||||
|
const total = typeof incoming.chunksTotal === 'number' ? incoming.chunksTotal : 0
|
||||||
|
const pieces = []
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const part = chunks[i]
|
||||||
|
if (typeof part !== 'string' || !part) {
|
||||||
|
ctx.console.error(
|
||||||
|
argv0 +
|
||||||
|
': missing chunk ' +
|
||||||
|
i +
|
||||||
|
'/' +
|
||||||
|
total +
|
||||||
|
' (run `' +
|
||||||
|
argv0 +
|
||||||
|
' status ' +
|
||||||
|
incoming.transferId +
|
||||||
|
'` to inspect)'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pieces.push(part)
|
||||||
|
}
|
||||||
|
const buf = ctx.b4a.from(pieces.join(''), 'base64')
|
||||||
|
await ctx.vfs.writeFile(dest, buf)
|
||||||
|
incoming.status = 'saved'
|
||||||
|
incoming.savedTo = dest
|
||||||
|
incoming.savedAtMs = Date.now()
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, incoming.transferId)
|
||||||
|
tr.status = 'saved'
|
||||||
|
tr.chunksReceived = total
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'complete', {
|
||||||
|
offerId,
|
||||||
|
transferId: incoming.transferId,
|
||||||
|
savedTo: dest
|
||||||
|
})
|
||||||
|
ctx.console.log('saved ' + offerId + ' -> ' + dest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'status') {
|
||||||
|
const transferId = String(args[1] || '').trim()
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
for (const row of rows) {
|
||||||
|
const kind = row.packet?.kind
|
||||||
|
const p = row.packet?.payload
|
||||||
|
if (!p || typeof p !== 'object') continue
|
||||||
|
const id = typeof p.transferId === 'string' ? p.transferId : ''
|
||||||
|
if (transferId && id !== transferId) continue
|
||||||
|
if (kind === 'chunk' && typeof p.offerId === 'string' && typeof p.index === 'number') {
|
||||||
|
const inRec = db?.incoming?.[p.offerId]
|
||||||
|
if (inRec && inRec.status !== 'cancelled' && inRec.status !== 'saved') {
|
||||||
|
if (!inRec.chunks || typeof inRec.chunks !== 'object') inRec.chunks = {}
|
||||||
|
if (typeof p.chunkB64 === 'string' && !inRec.chunks[p.index]) {
|
||||||
|
inRec.chunks[p.index] = p.chunkB64
|
||||||
|
}
|
||||||
|
const got = Object.keys(inRec.chunks).length
|
||||||
|
inRec.status = got >= inRec.chunksTotal ? 'received-all' : 'receiving'
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, inRec.transferId)
|
||||||
|
tr.status = inRec.status
|
||||||
|
tr.chunksTotal = inRec.chunksTotal
|
||||||
|
tr.chunksReceived = got
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kind === 'cancel' && id) {
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, id)
|
||||||
|
tr.status = 'cancelled'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
if (kind === 'progress' && id) {
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, id)
|
||||||
|
if (typeof p.sentChunks === 'number') tr.sentChunks = p.sentChunks
|
||||||
|
if (typeof p.chunksTotal === 'number') tr.chunksTotal = p.chunksTotal
|
||||||
|
tr.status = 'sending'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
if (kind === 'complete' && id) {
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, id)
|
||||||
|
tr.status = 'complete'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
const items = Object.values(db.transfers || {})
|
||||||
|
.filter((x) => !transferId || x.transferId === transferId)
|
||||||
|
.sort((a, b) => (b.updatedAtMs || 0) - (a.updatedAtMs || 0))
|
||||||
|
if (!items.length) {
|
||||||
|
ctx.console.log('no transfers')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const it of items) {
|
||||||
|
ctx.console.log(
|
||||||
|
(it.transferId || '?') +
|
||||||
|
' role=' +
|
||||||
|
String(it.role || '?') +
|
||||||
|
' status=' +
|
||||||
|
String(it.status || '?') +
|
||||||
|
' chunks=' +
|
||||||
|
String(it.chunksReceived || 0) +
|
||||||
|
'/' +
|
||||||
|
String(it.chunksTotal || 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'cancel') {
|
||||||
|
const transferId = String(args[1] || '').trim()
|
||||||
|
if (!transferId) {
|
||||||
|
ctx.console.error(argv0 + ': cancel requires TRANSFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, transferId)
|
||||||
|
tr.status = 'cancelled'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'cancel', { transferId })
|
||||||
|
ctx.console.log('cancelled ' + transferId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
/* 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 : []
|
||||||
|
out.peers = peers.slice(0, 128)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'peernote'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' add TEXT\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' list [N]\n' +
|
||||||
|
'Shared p2p note stream over bare-p2p envelopes.\n' +
|
||||||
|
'See man peernote.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'add') {
|
||||||
|
const text = args.slice(1).join(' ').trim()
|
||||||
|
if (!text) {
|
||||||
|
ctx.console.error(argv0 + ': add requires text')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const r = bareP2pSend(ctx, 'peernote', 'note.add', {
|
||||||
|
noteId: bareP2pId('note'),
|
||||||
|
text
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (sub === 'list') {
|
||||||
|
const n = Math.max(1, Math.min(300, parseInt(args[1] || '30', 10) || 30))
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'peernote', 2000)
|
||||||
|
.filter((r) => r.packet?.kind === 'note.add')
|
||||||
|
.slice(-n)
|
||||||
|
for (const row of rows) {
|
||||||
|
const p = row.packet?.payload || {}
|
||||||
|
const text = typeof p.text === 'string' ? p.text : ''
|
||||||
|
const who = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
|
ctx.console.log('[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + who + ': ' + text)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
+1017
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,387 @@
|
|||||||
|
/* 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 : []
|
||||||
|
out.peers = peers.slice(0, 128)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'taskmesh'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' add TEXT\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' done TASK_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' list [open|all]\n' +
|
||||||
|
'P2P task board over append-only bare-p2p events.\n' +
|
||||||
|
'See man taskmesh.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'add') {
|
||||||
|
const text = args.slice(1).join(' ').trim()
|
||||||
|
if (!text) {
|
||||||
|
ctx.console.error(argv0 + ': add requires text')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const taskId = bareP2pId('task')
|
||||||
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.add', {
|
||||||
|
taskId,
|
||||||
|
text
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log('added ' + taskId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'done') {
|
||||||
|
const taskId = String(args[1] || '').trim()
|
||||||
|
if (!taskId) {
|
||||||
|
ctx.console.error(argv0 + ': done requires TASK_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.done', { taskId })
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log('completed ' + taskId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'list') {
|
||||||
|
const mode = args[1] === 'all' ? 'all' : 'open'
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'taskmesh', 2000)
|
||||||
|
/** @type {Map<string, { taskId: string, text: string, done: boolean, atMs: number }>} */
|
||||||
|
const board = new Map()
|
||||||
|
for (const row of rows) {
|
||||||
|
const kind = row.packet?.kind
|
||||||
|
const p = row.packet?.payload
|
||||||
|
if (!p || typeof p !== 'object') continue
|
||||||
|
if (kind === 'task.add' && typeof p.taskId === 'string') {
|
||||||
|
board.set(p.taskId, {
|
||||||
|
taskId: p.taskId,
|
||||||
|
text: typeof p.text === 'string' ? p.text : '',
|
||||||
|
done: false,
|
||||||
|
atMs: row.receivedAtMs
|
||||||
|
})
|
||||||
|
} else if (kind === 'task.done' && typeof p.taskId === 'string') {
|
||||||
|
const cur = board.get(p.taskId)
|
||||||
|
if (cur) cur.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const all = [...board.values()].sort((a, b) => a.atMs - b.atMs)
|
||||||
|
for (const t of all) {
|
||||||
|
if (mode !== 'all' && t.done) continue
|
||||||
|
ctx.console.log((t.done ? '[x] ' : '[ ] ') + t.taskId + ' ' + t.text)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema": 2,
|
"schema": 2,
|
||||||
"profileId": "bare-os-posix-like",
|
"profileId": "bare-os-posix-like",
|
||||||
"generatedAt": "2026-04-26T10:19:42.583Z",
|
"generatedAt": "2026-04-26T11:47:34.024Z",
|
||||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||||
"commandIndex": [
|
"commandIndex": [
|
||||||
{
|
{
|
||||||
@@ -216,6 +216,10 @@
|
|||||||
"name": "hostname",
|
"name": "hostname",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "hypershell-board",
|
||||||
|
"tier": "tier1_bin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "iconv",
|
"name": "iconv",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
@@ -308,6 +312,10 @@
|
|||||||
"name": "md5sum",
|
"name": "md5sum",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "meshdrop",
|
||||||
|
"tier": "tier1_bin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "mkdir",
|
"name": "mkdir",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
@@ -380,6 +388,10 @@
|
|||||||
"name": "pathchk",
|
"name": "pathchk",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "peernote",
|
||||||
|
"tier": "tier1_bin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "pkg-swarm-index",
|
"name": "pkg-swarm-index",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
@@ -504,6 +516,10 @@
|
|||||||
"name": "sum",
|
"name": "sum",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "swarmtop",
|
||||||
|
"tier": "tier1_bin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "sync",
|
"name": "sync",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
@@ -520,6 +536,10 @@
|
|||||||
"name": "tar",
|
"name": "tar",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "taskmesh",
|
||||||
|
"tier": "tier1_bin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "tee",
|
"name": "tee",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"atMs": 1777198782582,
|
"atMs": 1777204054023,
|
||||||
"commands": [
|
"commands": [
|
||||||
"agent",
|
"agent",
|
||||||
"arch",
|
"arch",
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"holesail",
|
"holesail",
|
||||||
"hostid",
|
"hostid",
|
||||||
"hostname",
|
"hostname",
|
||||||
|
"hypershell-board",
|
||||||
"iconv",
|
"iconv",
|
||||||
"id",
|
"id",
|
||||||
"install",
|
"install",
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
"ls",
|
"ls",
|
||||||
"man",
|
"man",
|
||||||
"md5sum",
|
"md5sum",
|
||||||
|
"meshdrop",
|
||||||
"mkdir",
|
"mkdir",
|
||||||
"mkfifo",
|
"mkfifo",
|
||||||
"mktemp",
|
"mktemp",
|
||||||
@@ -96,6 +98,7 @@
|
|||||||
"patch",
|
"patch",
|
||||||
"pathcap-verify",
|
"pathcap-verify",
|
||||||
"pathchk",
|
"pathchk",
|
||||||
|
"peernote",
|
||||||
"pkg-swarm-index",
|
"pkg-swarm-index",
|
||||||
"pr",
|
"pr",
|
||||||
"printenv",
|
"printenv",
|
||||||
@@ -127,10 +130,12 @@
|
|||||||
"sshd",
|
"sshd",
|
||||||
"stat",
|
"stat",
|
||||||
"sum",
|
"sum",
|
||||||
|
"swarmtop",
|
||||||
"sync",
|
"sync",
|
||||||
"tac",
|
"tac",
|
||||||
"tail",
|
"tail",
|
||||||
"tar",
|
"tar",
|
||||||
|
"taskmesh",
|
||||||
"tee",
|
"tee",
|
||||||
"telnet",
|
"telnet",
|
||||||
"test",
|
"test",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -215,6 +215,10 @@ import {
|
|||||||
bareOsChatMuxEnabled,
|
bareOsChatMuxEnabled,
|
||||||
ensureDiskBareOsChatTransport
|
ensureDiskBareOsChatTransport
|
||||||
} from './lib/bare-os-chat-service.js'
|
} from './lib/bare-os-chat-service.js'
|
||||||
|
import {
|
||||||
|
bareOsMeshdropMuxEnabled,
|
||||||
|
ensureDiskBareOsMeshdropTransport
|
||||||
|
} from './lib/bare-os-meshdrop-service.js'
|
||||||
import { runSshdCli, getBareOpensshProcJsonText } from './lib/bare-openssh.js'
|
import { runSshdCli, getBareOpensshProcJsonText } from './lib/bare-openssh.js'
|
||||||
import { createHash } from 'bare-crypto'
|
import { createHash } from 'bare-crypto'
|
||||||
import { buildBareOsHostProcJsonText } from './lib/bare-os-host-proc-snapshot.js'
|
import { buildBareOsHostProcJsonText } from './lib/bare-os-host-proc-snapshot.js'
|
||||||
@@ -890,6 +894,9 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
if (bareOsChatMuxEnabled(globalThis.process?.env)) {
|
if (bareOsChatMuxEnabled(globalThis.process?.env)) {
|
||||||
ensureDiskBareOsChatTransport(disk, shellEnv)
|
ensureDiskBareOsChatTransport(disk, shellEnv)
|
||||||
}
|
}
|
||||||
|
if (bareOsMeshdropMuxEnabled(globalThis.process?.env)) {
|
||||||
|
ensureDiskBareOsMeshdropTransport(disk, shellEnv)
|
||||||
|
}
|
||||||
let bootProfileResolved = ''
|
let bootProfileResolved = ''
|
||||||
const bpfEarly = shellEnv.BARE_OS_BOOT_PROFILE
|
const bpfEarly = shellEnv.BARE_OS_BOOT_PROFILE
|
||||||
if (bpfEarly != null && String(bpfEarly).trim()) {
|
if (bpfEarly != null && String(bpfEarly).trim()) {
|
||||||
@@ -1241,6 +1248,34 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
}
|
}
|
||||||
return `${JSON.stringify(o)}\n`
|
return `${JSON.stringify(o)}\n`
|
||||||
},
|
},
|
||||||
|
procBareOsMeshdropText() {
|
||||||
|
const svc = disk.bareOsMeshdropService
|
||||||
|
/** @type {Record<string, unknown>} */
|
||||||
|
const o = svc
|
||||||
|
? {
|
||||||
|
schema: 1,
|
||||||
|
atMs: Date.now(),
|
||||||
|
protocol: svc.PROTOCOL_MESHDROP_CHANNEL_NAME,
|
||||||
|
metrics: svc.snapshotMetrics(),
|
||||||
|
protomuxMeshdropRxTotal:
|
||||||
|
typeof disk.protomuxMeshdropChannelRxTotal === 'number'
|
||||||
|
? disk.protomuxMeshdropChannelRxTotal
|
||||||
|
: 0,
|
||||||
|
swarmPeers:
|
||||||
|
disk.peers && typeof disk.peers.size === 'number'
|
||||||
|
? disk.peers.size
|
||||||
|
: 0,
|
||||||
|
muxEnabled:
|
||||||
|
bareOsMeshdropMuxEnabled(shellEnv) ||
|
||||||
|
bareOsMeshdropMuxEnabled(hostEnv || {})
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
schema: 1,
|
||||||
|
note:
|
||||||
|
'Meshdrop channel is off when BARE_OS_PROTOMUX_MESHDROP_CHANNEL=0/false/off (stock default is on).'
|
||||||
|
}
|
||||||
|
return `${JSON.stringify(o)}\n`
|
||||||
|
},
|
||||||
procBareOsClockText() {
|
procBareOsClockText() {
|
||||||
const wallMs = Date.now()
|
const wallMs = Date.now()
|
||||||
let monotonicNsFromPerf = null
|
let monotonicNsFromPerf = null
|
||||||
@@ -3391,6 +3426,7 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
'event',
|
'event',
|
||||||
'control'
|
'control'
|
||||||
],
|
],
|
||||||
|
bareOsMeshdropMessageKinds: ['envelope'],
|
||||||
note: 'Stable Protomux channel ordering for bare-os-v1 seed/booter wire; bare-os-chat-v1 is a separate mux pair.'
|
note: 'Stable Protomux channel ordering for bare-os-v1 seed/booter wire; bare-os-chat-v1 is a separate mux pair.'
|
||||||
},
|
},
|
||||||
operatorMetrics: {
|
operatorMetrics: {
|
||||||
@@ -3421,6 +3457,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
typeof disk.protomuxChatChannelRxTotal === 'number'
|
typeof disk.protomuxChatChannelRxTotal === 'number'
|
||||||
? disk.protomuxChatChannelRxTotal
|
? disk.protomuxChatChannelRxTotal
|
||||||
: 0,
|
: 0,
|
||||||
|
protomuxMeshdropChannelRxTotal:
|
||||||
|
typeof disk.protomuxMeshdropChannelRxTotal === 'number'
|
||||||
|
? disk.protomuxMeshdropChannelRxTotal
|
||||||
|
: 0,
|
||||||
rpcClientPoolHintPresent: !!(
|
rpcClientPoolHintPresent: !!(
|
||||||
disk.seedProtomuxRpcPoolHint &&
|
disk.seedProtomuxRpcPoolHint &&
|
||||||
typeof disk.seedProtomuxRpcPoolHint === 'object'
|
typeof disk.seedProtomuxRpcPoolHint === 'object'
|
||||||
@@ -3429,7 +3469,7 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
shellEnv.BARE_OS_PROTOMUX_RPC_CLIENT_POOL_COMPOSITION === '1' ||
|
shellEnv.BARE_OS_PROTOMUX_RPC_CLIENT_POOL_COMPOSITION === '1' ||
|
||||||
shellEnv.BARE_OS_PROTOMUX_RPC_CLIENT_POOL_COMPOSITION === 'true',
|
shellEnv.BARE_OS_PROTOMUX_RPC_CLIENT_POOL_COMPOSITION === 'true',
|
||||||
rpcClientPoolCompositionEnv: 'BARE_OS_PROTOMUX_RPC_CLIENT_POOL_COMPOSITION',
|
rpcClientPoolCompositionEnv: 'BARE_OS_PROTOMUX_RPC_CLIENT_POOL_COMPOSITION',
|
||||||
note: 'Schema 5: protomuxCapChannelRxTotal (bare-os-cap-v1). Schema 4: rpcClientPoolCompositionGate. Schema 3: protomuxAppChannelRxTotal.',
|
note: 'Schema 6: protomuxMeshdropChannelRxTotal (bare-os-meshdrop-v1). Schema 5: protomuxCapChannelRxTotal (bare-os-cap-v1). Schema 4: rpcClientPoolCompositionGate. Schema 3: protomuxAppChannelRxTotal.',
|
||||||
backpressureEvent: 'bare-os:protomux-backpressure',
|
backpressureEvent: 'bare-os:protomux-backpressure',
|
||||||
backpressureEmit: 'ctx.bareOsEmitProtomuxBackpressure'
|
backpressureEmit: 'ctx.bareOsEmitProtomuxBackpressure'
|
||||||
},
|
},
|
||||||
@@ -8334,6 +8374,41 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
bareOsChatRooms() {
|
bareOsChatRooms() {
|
||||||
return disk.bareOsChatService?.rooms() ?? []
|
return disk.bareOsChatService?.rooms() ?? []
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Dedicated meshdrop protomux channel send (`bare-os-meshdrop-v1`).
|
||||||
|
* `frame` is a JSON object with kind/payload fields understood by meshdrop(1).
|
||||||
|
*/
|
||||||
|
bareOsMeshdropSend(frame) {
|
||||||
|
const ms = disk.bareOsMeshdropService
|
||||||
|
if (!ms || !frame || typeof frame !== 'object') {
|
||||||
|
return { ok: false, reason: 'disabled' }
|
||||||
|
}
|
||||||
|
const r = ms.broadcastLocal(disk, /** @type {Record<string, unknown>} */ (frame), {
|
||||||
|
sender: String(shellEnv.USER || 'guest')
|
||||||
|
})
|
||||||
|
return r && typeof r === 'object' ? r : { ok: false, reason: 'send_failed' }
|
||||||
|
},
|
||||||
|
bareOsMeshdropSubscribe(fn) {
|
||||||
|
const ms = disk.bareOsMeshdropService
|
||||||
|
if (!ms || typeof fn !== 'function') return () => {}
|
||||||
|
return ms.subscribe(fn)
|
||||||
|
},
|
||||||
|
bareOsMeshdropHistory(limit) {
|
||||||
|
const ms = disk.bareOsMeshdropService
|
||||||
|
if (!ms || typeof ms.history !== 'function') return []
|
||||||
|
const all = ms.history()
|
||||||
|
let n = 1024
|
||||||
|
if (
|
||||||
|
typeof limit === 'number' &&
|
||||||
|
Number.isFinite(limit) &&
|
||||||
|
limit >= 1 &&
|
||||||
|
limit <= 10000
|
||||||
|
) {
|
||||||
|
n = Math.floor(limit)
|
||||||
|
}
|
||||||
|
if (all.length <= n) return all
|
||||||
|
return all.slice(all.length - n)
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Parse `/proc/bare_os/replication` (same JSON as the VFS pseudo file).
|
* Parse `/proc/bare_os/replication` (same JSON as the VFS pseudo file).
|
||||||
* @returns {Promise<Record<string, unknown> | null>}
|
* @returns {Promise<Record<string, unknown> | null>}
|
||||||
@@ -8444,6 +8519,7 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
['processTable', '/proc/bare_os/process_table.json'],
|
['processTable', '/proc/bare_os/process_table.json'],
|
||||||
['syscalls', '/proc/bare_os/syscalls.json'],
|
['syscalls', '/proc/bare_os/syscalls.json'],
|
||||||
['metricsProm', '/proc/bare_os/metrics.prom'],
|
['metricsProm', '/proc/bare_os/metrics.prom'],
|
||||||
|
['meshdrop', '/proc/bare_os/meshdrop.json'],
|
||||||
['protomuxWire', '/proc/bare_os/protomux.json'],
|
['protomuxWire', '/proc/bare_os/protomux.json'],
|
||||||
['securityPosture', '/proc/bare_os/security_posture.json'],
|
['securityPosture', '/proc/bare_os/security_posture.json'],
|
||||||
['processIo', '/proc/bare_os/process_io.json'],
|
['processIo', '/proc/bare_os/process_io.json'],
|
||||||
@@ -8456,6 +8532,7 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
['hostOs', '/proc/bare_os/host_os.json'],
|
['hostOs', '/proc/bare_os/host_os.json'],
|
||||||
['replication', '/proc/bare_os/replication'],
|
['replication', '/proc/bare_os/replication'],
|
||||||
['swarm', '/proc/bare_os/swarm'],
|
['swarm', '/proc/bare_os/swarm'],
|
||||||
|
['meshdrop', '/proc/bare_os/meshdrop.json'],
|
||||||
['syncWindow', '/proc/bare_os/sync_window.json'],
|
['syncWindow', '/proc/bare_os/sync_window.json'],
|
||||||
['processTable', '/proc/bare_os/process_table.json'],
|
['processTable', '/proc/bare_os/process_table.json'],
|
||||||
['snapshotHints', '/proc/bare_os/snapshot_hints.json']
|
['snapshotHints', '/proc/bare_os/snapshot_hints.json']
|
||||||
@@ -10313,7 +10390,7 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
? disk.swarmConnectionBudget
|
? disk.swarmConnectionBudget
|
||||||
: null,
|
: null,
|
||||||
protomuxOperatorSketch: {
|
protomuxOperatorSketch: {
|
||||||
schema: 4,
|
schema: 5,
|
||||||
protomuxAppChannelRxTotal:
|
protomuxAppChannelRxTotal:
|
||||||
typeof disk.protomuxAppChannelRxTotal === 'number'
|
typeof disk.protomuxAppChannelRxTotal === 'number'
|
||||||
? disk.protomuxAppChannelRxTotal
|
? disk.protomuxAppChannelRxTotal
|
||||||
@@ -10322,6 +10399,10 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
typeof disk.protomuxCapChannelRxTotal === 'number'
|
typeof disk.protomuxCapChannelRxTotal === 'number'
|
||||||
? disk.protomuxCapChannelRxTotal
|
? disk.protomuxCapChannelRxTotal
|
||||||
: 0,
|
: 0,
|
||||||
|
protomuxMeshdropChannelRxTotal:
|
||||||
|
typeof disk.protomuxMeshdropChannelRxTotal === 'number'
|
||||||
|
? disk.protomuxMeshdropChannelRxTotal
|
||||||
|
: 0,
|
||||||
envGateAppChannel: 'BARE_OS_PROTOMUX_APP_CHANNEL',
|
envGateAppChannel: 'BARE_OS_PROTOMUX_APP_CHANNEL',
|
||||||
envGateCapChannel: 'BARE_OS_PROTOMUX_CAP_CHANNEL',
|
envGateCapChannel: 'BARE_OS_PROTOMUX_CAP_CHANNEL',
|
||||||
tuningFromEnv: bareOsProtomuxTuningFromEnv(shellEnv),
|
tuningFromEnv: bareOsProtomuxTuningFromEnv(shellEnv),
|
||||||
@@ -10332,7 +10413,7 @@ async function executeKernel(disk, store, swarm, initSource) {
|
|||||||
? disk.peers.size
|
? disk.peers.size
|
||||||
: null,
|
: null,
|
||||||
note:
|
note:
|
||||||
'Schema 4: multiplexPeerCountEcho + protomuxWire correlation with swarm peers. Schema 3: protomuxCapChannelRxTotal + BARE_OS_PROTOMUX_CAP_CHANNEL.'
|
'Schema 5 adds protomuxMeshdropChannelRxTotal (bare-os-meshdrop-v1). Schema 4: multiplexPeerCountEcho + protomuxWire correlation with swarm peers. Schema 3: protomuxCapChannelRxTotal + BARE_OS_PROTOMUX_CAP_CHANNEL.'
|
||||||
},
|
},
|
||||||
auditBatch: (entries) => {
|
auditBatch: (entries) => {
|
||||||
try {
|
try {
|
||||||
@@ -10504,6 +10585,14 @@ async function main() {
|
|||||||
...(hostEnvMain && typeof hostEnvMain === 'object' ? hostEnvMain : {})
|
...(hostEnvMain && typeof hostEnvMain === 'object' ? hostEnvMain : {})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (bareOsMeshdropMuxEnabled(hostEnvMain)) {
|
||||||
|
ensureDiskBareOsMeshdropTransport(disk, {
|
||||||
|
HOME: '/home/guest',
|
||||||
|
USER: 'guest',
|
||||||
|
LOGNAME: 'guest',
|
||||||
|
...(hostEnvMain && typeof hostEnvMain === 'object' ? hostEnvMain : {})
|
||||||
|
})
|
||||||
|
}
|
||||||
disk.swarmConnectionBudget = {
|
disk.swarmConnectionBudget = {
|
||||||
schema: 1,
|
schema: 1,
|
||||||
requestedFromEnv: swarmOpts,
|
requestedFromEnv: swarmOpts,
|
||||||
|
|||||||
+11
@@ -149,6 +149,17 @@ export interface BareOsKernelContext {
|
|||||||
bareOsChatPresence?(): Record<string, unknown>
|
bareOsChatPresence?(): Record<string, unknown>
|
||||||
/** Room id list (v1: `general` only). */
|
/** Room id list (v1: `general` only). */
|
||||||
bareOsChatRooms?(): string[]
|
bareOsChatRooms?(): string[]
|
||||||
|
/** Meshdrop frame send over dedicated `bare-os-meshdrop-v1` channel. */
|
||||||
|
bareOsMeshdropSend?(frame: Record<string, unknown>): {
|
||||||
|
ok: boolean
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
/** Subscribe to meshdrop envelopes. */
|
||||||
|
bareOsMeshdropSubscribe?(
|
||||||
|
fn: (ev: Record<string, unknown>) => void
|
||||||
|
): () => void
|
||||||
|
/** Recent meshdrop envelopes (ring buffer on host; optional max length). */
|
||||||
|
bareOsMeshdropHistory?(limit?: number): Record<string, unknown>[]
|
||||||
bareOsReadReplicationOperatorJson?(): Promise<Record<string, unknown> | null>
|
bareOsReadReplicationOperatorJson?(): Promise<Record<string, unknown> | null>
|
||||||
bareOsInvalidateWarmReadCaches?(
|
bareOsInvalidateWarmReadCaches?(
|
||||||
reason?: string
|
reason?: string
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import {
|
||||||
|
setupBareOsMeshdropChannel,
|
||||||
|
PROTOCOL_MESHDROP_CHANNEL_NAME,
|
||||||
|
BARE_OS_MESHDROP_WIRE_SCHEMA_VERSION,
|
||||||
|
bareOsProtMuxMeshdropChannelEnabled
|
||||||
|
} from 'bare-os-protocol'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{ chan: import('protomux').Channel, mux: import('protomux').Protomux, socket: any, id: string | null, meshdropChan?: import('protomux').Channel | null }} SwarmPeer
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, string | undefined>} [env]
|
||||||
|
*/
|
||||||
|
export function bareOsMeshdropMuxEnabled(env = globalThis.process?.env) {
|
||||||
|
return bareOsProtMuxMeshdropChannelEnabled(env || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('./swarm-disk.js').SwarmDisk} disk
|
||||||
|
* @param {Record<string, string | undefined>} [env]
|
||||||
|
*/
|
||||||
|
export function ensureDiskBareOsMeshdropTransport(disk, env = {}) {
|
||||||
|
if (!disk || !bareOsMeshdropMuxEnabled(globalThis.process?.env)) return
|
||||||
|
const merged = {
|
||||||
|
.../** @type {Record<string, string | undefined>} */ (
|
||||||
|
globalThis.process?.env || {}
|
||||||
|
),
|
||||||
|
...env
|
||||||
|
}
|
||||||
|
disk.bareOsMeshdropService = createBareOsMeshdropService({ env: merged })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, string | undefined>} env
|
||||||
|
*/
|
||||||
|
function meshdropHistoryMax(env) {
|
||||||
|
const raw = String(env.BARE_OS_MESHDROP_HISTORY_MAX ?? '').trim()
|
||||||
|
const n = raw ? Number.parseInt(raw, 10) : NaN
|
||||||
|
if (Number.isFinite(n) && n >= 32 && n <= 10000) return n
|
||||||
|
return 2048
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, string | undefined>} env
|
||||||
|
*/
|
||||||
|
function meshdropPayloadMaxBytes(env) {
|
||||||
|
const raw = String(env.BARE_OS_MESHDROP_MAX_PAYLOAD_BYTES ?? '').trim()
|
||||||
|
const n = raw ? Number.parseInt(raw, 10) : NaN
|
||||||
|
if (Number.isFinite(n) && n >= 512 && n <= 512 * 1024) return n
|
||||||
|
return 64 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBareOsMeshdropService(opts = {}) {
|
||||||
|
const env = opts.env || globalThis.process?.env || {}
|
||||||
|
const historyMax = meshdropHistoryMax(
|
||||||
|
/** @type {Record<string, string | undefined>} */ (env)
|
||||||
|
)
|
||||||
|
const payloadMaxBytes = meshdropPayloadMaxBytes(
|
||||||
|
/** @type {Record<string, string | undefined>} */ (env)
|
||||||
|
)
|
||||||
|
/** @type {Set<(ev: Record<string, unknown>) => void>} */
|
||||||
|
const subscribers = new Set()
|
||||||
|
/** @type {Array<Record<string, unknown>>} */
|
||||||
|
const history = []
|
||||||
|
/** @type {Map<string, number>} */
|
||||||
|
const seenFrame = new Map()
|
||||||
|
const metrics = {
|
||||||
|
rxEnvelope: 0,
|
||||||
|
txEnvelope: 0,
|
||||||
|
droppedPayload: 0,
|
||||||
|
deduped: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimSeen() {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const [k, exp] of seenFrame) {
|
||||||
|
if (exp < now) seenFrame.delete(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pushHistory(rec) {
|
||||||
|
history.push(rec)
|
||||||
|
while (history.length > historyMax) history.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Record<string, unknown>} frame
|
||||||
|
*/
|
||||||
|
function frameId(frame) {
|
||||||
|
const p = frame && typeof frame.payload === 'object' ? frame.payload : {}
|
||||||
|
const transferId = typeof p.transferId === 'string' ? p.transferId : ''
|
||||||
|
const offerId = typeof p.offerId === 'string' ? p.offerId : ''
|
||||||
|
const idx = typeof p.index === 'number' ? p.index : -1
|
||||||
|
const kind = typeof frame.kind === 'string' ? frame.kind : 'unknown'
|
||||||
|
const frameUid = typeof p.frameId === 'string' ? p.frameId : ''
|
||||||
|
return frameUid || `${transferId}:${offerId}:${kind}:${idx}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('./swarm-disk.js').SwarmDisk} disk
|
||||||
|
* @param {SwarmPeer} fromPeer
|
||||||
|
* @param {Record<string, unknown>} frame
|
||||||
|
*/
|
||||||
|
function relayEnvelope(disk, fromPeer, frame) {
|
||||||
|
for (const p of disk.peers) {
|
||||||
|
if (p === fromPeer) continue
|
||||||
|
const ch = p.meshdropChan
|
||||||
|
if (!ch || !ch.messages || !ch.messages[0]) continue
|
||||||
|
try {
|
||||||
|
ch.messages[0].send(frame)
|
||||||
|
metrics.txEnvelope++
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import('./swarm-disk.js').SwarmDisk} disk
|
||||||
|
* @param {SwarmPeer} fromPeer
|
||||||
|
* @param {Record<string, unknown>} frame
|
||||||
|
*/
|
||||||
|
function ingestEnvelope(disk, fromPeer, frame) {
|
||||||
|
if (!frame || typeof frame !== 'object') return
|
||||||
|
const encodedLen = JSON.stringify(frame).length
|
||||||
|
if (encodedLen > payloadMaxBytes) {
|
||||||
|
metrics.droppedPayload++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trimSeen()
|
||||||
|
const fid = frameId(frame)
|
||||||
|
if (fid) {
|
||||||
|
if (seenFrame.has(fid)) {
|
||||||
|
metrics.deduped++
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenFrame.set(fid, Date.now() + 120_000)
|
||||||
|
}
|
||||||
|
metrics.rxEnvelope++
|
||||||
|
const rec = {
|
||||||
|
...frame,
|
||||||
|
fromPeerKey: fromPeer.id || '',
|
||||||
|
receivedAtMs: Date.now()
|
||||||
|
}
|
||||||
|
pushHistory(rec)
|
||||||
|
for (const fn of subscribers) {
|
||||||
|
try {
|
||||||
|
fn(rec)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
relayEnvelope(disk, fromPeer, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
PROTOCOL_MESHDROP_CHANNEL_NAME,
|
||||||
|
metrics,
|
||||||
|
history() {
|
||||||
|
return [...history]
|
||||||
|
},
|
||||||
|
subscribe(fn) {
|
||||||
|
subscribers.add(fn)
|
||||||
|
return () => subscribers.delete(fn)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {import('./swarm-disk.js').SwarmDisk} disk
|
||||||
|
* @param {import('protomux').Protomux} mux
|
||||||
|
* @param {any} _socket
|
||||||
|
* @param {SwarmPeer} peer
|
||||||
|
*/
|
||||||
|
pairOnMux(disk, mux, _socket, peer) {
|
||||||
|
setupBareOsMeshdropChannel(mux, {
|
||||||
|
onEnvelope(m, _ch) {
|
||||||
|
try {
|
||||||
|
disk.protomuxMeshdropChannelRxTotal =
|
||||||
|
(disk.protomuxMeshdropChannelRxTotal || 0) + 1
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
ingestEnvelope(disk, peer, m)
|
||||||
|
},
|
||||||
|
onChannelOpened(chan) {
|
||||||
|
peer.meshdropChan = chan
|
||||||
|
mux.stream?.once?.('close', () => {
|
||||||
|
peer.meshdropChan = null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param {import('./swarm-disk.js').SwarmDisk} disk
|
||||||
|
* @param {Record<string, unknown>} frame
|
||||||
|
* @param {{ sender?: string }} [meta]
|
||||||
|
*/
|
||||||
|
broadcastLocal(disk, frame, meta = {}) {
|
||||||
|
const safeFrame = {
|
||||||
|
schemaVersion: BARE_OS_MESHDROP_WIRE_SCHEMA_VERSION,
|
||||||
|
sender: String(meta.sender || env.USER || 'local'),
|
||||||
|
tsMs: Date.now(),
|
||||||
|
...frame
|
||||||
|
}
|
||||||
|
const encodedLen = JSON.stringify(safeFrame).length
|
||||||
|
if (encodedLen > payloadMaxBytes) {
|
||||||
|
metrics.droppedPayload++
|
||||||
|
return { ok: false, reason: 'payload_too_large' }
|
||||||
|
}
|
||||||
|
pushHistory({ ...safeFrame, local: true, receivedAtMs: Date.now() })
|
||||||
|
for (const fn of subscribers) {
|
||||||
|
try {
|
||||||
|
fn({ ...safeFrame, local: true })
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const p of disk.peers) {
|
||||||
|
const ch = p.meshdropChan
|
||||||
|
if (!ch || !ch.messages || !ch.messages[0]) continue
|
||||||
|
try {
|
||||||
|
ch.messages[0].send(safeFrame)
|
||||||
|
metrics.txEnvelope++
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: true }
|
||||||
|
},
|
||||||
|
snapshotMetrics() {
|
||||||
|
return {
|
||||||
|
...metrics,
|
||||||
|
historyMax,
|
||||||
|
payloadMaxBytes,
|
||||||
|
protocol: PROTOCOL_MESHDROP_CHANNEL_NAME
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,10 @@ import {
|
|||||||
bareOsChatMuxEnabled,
|
bareOsChatMuxEnabled,
|
||||||
ensureDiskBareOsChatTransport
|
ensureDiskBareOsChatTransport
|
||||||
} from './bare-os-chat-service.js'
|
} from './bare-os-chat-service.js'
|
||||||
|
import {
|
||||||
|
bareOsMeshdropMuxEnabled,
|
||||||
|
ensureDiskBareOsMeshdropTransport
|
||||||
|
} from './bare-os-meshdrop-service.js'
|
||||||
|
|
||||||
/** @param {Record<string, string | undefined>} env */
|
/** @param {Record<string, string | undefined>} env */
|
||||||
export function registerBareUserSessionStackUnits(env) {
|
export function registerBareUserSessionStackUnits(env) {
|
||||||
@@ -44,12 +48,22 @@ export async function startBareUserSessionStack(ctx) {
|
|||||||
env
|
env
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (disk && bareOsMeshdropMuxEnabled(globalThis.process?.env)) {
|
||||||
|
ensureDiskBareOsMeshdropTransport(
|
||||||
|
/** @type {import('./swarm-disk.js').SwarmDisk} */ (disk),
|
||||||
|
env
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Peers may have connected during guest boot; pair chat after this tick (avoid mux re-entrancy). */
|
/** Peers may have connected during guest boot; pair chat after this tick (avoid mux re-entrancy). */
|
||||||
if (disk && typeof disk.pairBareOsChatExistingPeers === 'function') {
|
if (disk && typeof disk.pairBareOsChatExistingPeers === 'function') {
|
||||||
await new Promise((r) => setImmediate(r))
|
await new Promise((r) => setImmediate(r))
|
||||||
disk.pairBareOsChatExistingPeers()
|
disk.pairBareOsChatExistingPeers()
|
||||||
}
|
}
|
||||||
|
if (disk && typeof disk.pairBareOsMeshdropExistingPeers === 'function') {
|
||||||
|
await new Promise((r) => setImmediate(r))
|
||||||
|
disk.pairBareOsMeshdropExistingPeers()
|
||||||
|
}
|
||||||
|
|
||||||
const order = ['bare-os-www', 'bare-holesail', 'bare-os-chat']
|
const order = ['bare-os-www', 'bare-holesail', 'bare-os-chat']
|
||||||
for (const name of order) {
|
for (const name of order) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
stopBareUserSessionStack
|
stopBareUserSessionStack
|
||||||
} from './bare-user-session-stack.js'
|
} from './bare-user-session-stack.js'
|
||||||
import { ensureDiskBareOsChatTransport } from './bare-os-chat-service.js'
|
import { ensureDiskBareOsChatTransport } from './bare-os-chat-service.js'
|
||||||
|
import { ensureDiskBareOsMeshdropTransport } from './bare-os-meshdrop-service.js'
|
||||||
import { ensureBareOsWwwHomeDefaults } from './bare-os-www-initd.js'
|
import { ensureBareOsWwwHomeDefaults } from './bare-os-www-initd.js'
|
||||||
|
|
||||||
const LEGACY_ROOT_MIGRATION_STATE = '/.bare-os/migration/legacy-root-v1.json'
|
const LEGACY_ROOT_MIGRATION_STATE = '/.bare-os/migration/legacy-root-v1.json'
|
||||||
@@ -594,10 +595,17 @@ export async function logoutIdentity(ctx, opts = {}) {
|
|||||||
/** @type {import('./swarm-disk.js').SwarmDisk} */ (ctx.disk),
|
/** @type {import('./swarm-disk.js').SwarmDisk} */ (ctx.disk),
|
||||||
/** @type {Record<string, string | undefined>} */ (ctx.vfs.env)
|
/** @type {Record<string, string | undefined>} */ (ctx.vfs.env)
|
||||||
)
|
)
|
||||||
|
ensureDiskBareOsMeshdropTransport(
|
||||||
|
/** @type {import('./swarm-disk.js').SwarmDisk} */ (ctx.disk),
|
||||||
|
/** @type {Record<string, string | undefined>} */ (ctx.vfs.env)
|
||||||
|
)
|
||||||
await new Promise((r) => setImmediate(r))
|
await new Promise((r) => setImmediate(r))
|
||||||
if (typeof ctx.disk.pairBareOsChatExistingPeers === 'function') {
|
if (typeof ctx.disk.pairBareOsChatExistingPeers === 'function') {
|
||||||
ctx.disk.pairBareOsChatExistingPeers()
|
ctx.disk.pairBareOsChatExistingPeers()
|
||||||
}
|
}
|
||||||
|
if (typeof ctx.disk.pairBareOsMeshdropExistingPeers === 'function') {
|
||||||
|
ctx.disk.pairBareOsMeshdropExistingPeers()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.console?.error?.(
|
ctx.console?.error?.(
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import {
|
|||||||
PROTOCOL_NAME,
|
PROTOCOL_NAME,
|
||||||
PROTOCOL_APP_CHANNEL_NAME,
|
PROTOCOL_APP_CHANNEL_NAME,
|
||||||
PROTOCOL_CAP_CHANNEL_NAME,
|
PROTOCOL_CAP_CHANNEL_NAME,
|
||||||
PROTOCOL_CHAT_CHANNEL_NAME
|
PROTOCOL_CHAT_CHANNEL_NAME,
|
||||||
|
PROTOCOL_MESHDROP_CHANNEL_NAME
|
||||||
} from 'bare-os-protocol/constants.js'
|
} from 'bare-os-protocol/constants.js'
|
||||||
import { bareOsChatMuxEnabled } from './bare-os-chat-service.js'
|
import { bareOsChatMuxEnabled } from './bare-os-chat-service.js'
|
||||||
|
import { bareOsMeshdropMuxEnabled } from './bare-os-meshdrop-service.js'
|
||||||
import {
|
import {
|
||||||
bareOsHostBooterInfo,
|
bareOsHostBooterInfo,
|
||||||
bareOsHostBooterWarn
|
bareOsHostBooterWarn
|
||||||
@@ -142,8 +144,12 @@ export class SwarmDisk {
|
|||||||
this.protomuxCapChannelRxTotal = 0
|
this.protomuxCapChannelRxTotal = 0
|
||||||
/** Cumulative chat `event` messages received on `bare-os-chat-v1` (when enabled). */
|
/** Cumulative chat `event` messages received on `bare-os-chat-v1` (when enabled). */
|
||||||
this.protomuxChatChannelRxTotal = 0
|
this.protomuxChatChannelRxTotal = 0
|
||||||
|
/** Cumulative meshdrop envelopes received on `bare-os-meshdrop-v1` (when enabled). */
|
||||||
|
this.protomuxMeshdropChannelRxTotal = 0
|
||||||
/** @type {ReturnType<import('./bare-os-chat-service.js').createBareOsChatService> | null} */
|
/** @type {ReturnType<import('./bare-os-chat-service.js').createBareOsChatService> | null} */
|
||||||
this.bareOsChatService = null
|
this.bareOsChatService = null
|
||||||
|
/** @type {ReturnType<import('./bare-os-meshdrop-service.js').createBareOsMeshdropService> | null} */
|
||||||
|
this.bareOsMeshdropService = null
|
||||||
/** @type {Record<string, unknown> | null} Host-requested Hyperswarm caps (from env); surfaced on disk.os RPC. */
|
/** @type {Record<string, unknown> | null} Host-requested Hyperswarm caps (from env); surfaced on disk.os RPC. */
|
||||||
this.swarmConnectionBudget = null
|
this.swarmConnectionBudget = null
|
||||||
/** @type {Record<string, unknown> | null} */
|
/** @type {Record<string, unknown> | null} */
|
||||||
@@ -540,7 +546,14 @@ export class SwarmDisk {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const peer = { chan, mux, socket, id: null, chatChan: null }
|
const peer = {
|
||||||
|
chan,
|
||||||
|
mux,
|
||||||
|
socket,
|
||||||
|
id: null,
|
||||||
|
chatChan: null,
|
||||||
|
meshdropChan: null
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
bareOsChatMuxEnabled(globalThis.process?.env) &&
|
bareOsChatMuxEnabled(globalThis.process?.env) &&
|
||||||
this.bareOsChatService &&
|
this.bareOsChatService &&
|
||||||
@@ -555,6 +568,20 @@ export class SwarmDisk {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
bareOsMeshdropMuxEnabled(globalThis.process?.env) &&
|
||||||
|
this.bareOsMeshdropService &&
|
||||||
|
mux.stream &&
|
||||||
|
!mux.stream.destroyed
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
this.bareOsMeshdropService.pairOnMux(this, mux, socket, peer)
|
||||||
|
} catch (e) {
|
||||||
|
emitSwarmDiskHostLog('warn', 'bare_os_meshdrop_pair_on_connect_failed', {
|
||||||
|
message: (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Sync local Noise static key once the secret stream exposes `publicKey` (may follow handshake). */
|
/** Sync local Noise static key once the secret stream exposes `publicKey` (may follow handshake). */
|
||||||
const cacheLocalNoiseWirePk = () => {
|
const cacheLocalNoiseWirePk = () => {
|
||||||
@@ -726,4 +753,60 @@ export class SwarmDisk {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (Re)pair `bare-os-meshdrop-v1` on every live mux — used after meshdrop service env/user transitions.
|
||||||
|
*/
|
||||||
|
pairBareOsMeshdropExistingPeers() {
|
||||||
|
if (!bareOsMeshdropMuxEnabled(globalThis.process?.env)) return
|
||||||
|
const svc = this.bareOsMeshdropService
|
||||||
|
if (!svc || typeof svc.pairOnMux !== 'function') return
|
||||||
|
for (const peer of [...this.peers]) {
|
||||||
|
const st = peer.mux && peer.mux.stream
|
||||||
|
if (!st || st.destroyed) continue
|
||||||
|
try {
|
||||||
|
const mux = peer.mux
|
||||||
|
const wired = peer.meshdropChan
|
||||||
|
if (wired && typeof wired.close === 'function') {
|
||||||
|
try {
|
||||||
|
wired.close()
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
peer.meshdropChan = null
|
||||||
|
const prevCh =
|
||||||
|
mux &&
|
||||||
|
typeof mux.getLastChannel === 'function' &&
|
||||||
|
mux.getLastChannel({ protocol: PROTOCOL_MESHDROP_CHANNEL_NAME })
|
||||||
|
if (
|
||||||
|
prevCh &&
|
||||||
|
typeof prevCh.close === 'function' &&
|
||||||
|
prevCh !== wired
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
prevCh.close()
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
svc.pairOnMux(this, peer.mux, peer.socket, peer)
|
||||||
|
if (typeof peer.mux.unpair === 'function') {
|
||||||
|
try {
|
||||||
|
peer.mux.unpair({ protocol: PROTOCOL_MESHDROP_CHANNEL_NAME })
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emitSwarmDiskHostLog('warn', 'bare_os_meshdrop_late_pair_failed', {
|
||||||
|
message: (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -365,6 +365,7 @@ export const BARE_OS_PROC_FILE_TO_ID_HYPERCORE_PACK_HRPC_LIFECYCLE = Object.free
|
|||||||
* procBareOsBootBudgetSummaryText?: () => string,
|
* procBareOsBootBudgetSummaryText?: () => string,
|
||||||
* procBareOsMetricsLiveText?: () => string,
|
* procBareOsMetricsLiveText?: () => string,
|
||||||
* procBareOsChatText?: () => string,
|
* procBareOsChatText?: () => string,
|
||||||
|
* procBareOsMeshdropText?: () => string,
|
||||||
* procBareOsProcessTableText?: () => string,
|
* procBareOsProcessTableText?: () => string,
|
||||||
* procBareOsProcessIoText?: () => string,
|
* procBareOsProcessIoText?: () => string,
|
||||||
* procBareOsProcessThreadsText?: () => string,
|
* procBareOsProcessThreadsText?: () => string,
|
||||||
@@ -588,6 +589,10 @@ export function createVfs(
|
|||||||
typeof vfsOptions.procBareOsChatText === 'function'
|
typeof vfsOptions.procBareOsChatText === 'function'
|
||||||
? vfsOptions.procBareOsChatText
|
? vfsOptions.procBareOsChatText
|
||||||
: null
|
: null
|
||||||
|
const procBareOsMeshdropText =
|
||||||
|
typeof vfsOptions.procBareOsMeshdropText === 'function'
|
||||||
|
? vfsOptions.procBareOsMeshdropText
|
||||||
|
: null
|
||||||
const procBareOsProcessTableText =
|
const procBareOsProcessTableText =
|
||||||
typeof vfsOptions.procBareOsProcessTableText === 'function'
|
typeof vfsOptions.procBareOsProcessTableText === 'function'
|
||||||
? vfsOptions.procBareOsProcessTableText
|
? vfsOptions.procBareOsProcessTableText
|
||||||
@@ -1587,6 +1592,10 @@ export function createVfs(
|
|||||||
const t = procBareOsChatText ? procBareOsChatText() : '{}\n'
|
const t = procBareOsChatText ? procBareOsChatText() : '{}\n'
|
||||||
return utf8Encode(t)
|
return utf8Encode(t)
|
||||||
}
|
}
|
||||||
|
if (f === 'bare_os_meshdrop') {
|
||||||
|
const t = procBareOsMeshdropText ? procBareOsMeshdropText() : '{}\n'
|
||||||
|
return utf8Encode(t)
|
||||||
|
}
|
||||||
if (f === 'bare_os_process_table') {
|
if (f === 'bare_os_process_table') {
|
||||||
const t = procBareOsProcessTableText
|
const t = procBareOsProcessTableText
|
||||||
? procBareOsProcessTableText()
|
? procBareOsProcessTableText()
|
||||||
@@ -1877,6 +1886,7 @@ export function createVfs(
|
|||||||
path: '/proc/bare_os/metrics_live.json'
|
path: '/proc/bare_os/metrics_live.json'
|
||||||
},
|
},
|
||||||
{ name: 'chat.json', path: '/proc/bare_os/chat.json' },
|
{ name: 'chat.json', path: '/proc/bare_os/chat.json' },
|
||||||
|
{ name: 'meshdrop.json', path: '/proc/bare_os/meshdrop.json' },
|
||||||
{
|
{
|
||||||
name: 'process_table.json',
|
name: 'process_table.json',
|
||||||
path: '/proc/bare_os/process_table.json'
|
path: '/proc/bare_os/process_table.json'
|
||||||
@@ -2626,6 +2636,8 @@ export function createVfs(
|
|||||||
'metrics_live.json': 'bare_os_metrics_live',
|
'metrics_live.json': 'bare_os_metrics_live',
|
||||||
chat: 'bare_os_chat',
|
chat: 'bare_os_chat',
|
||||||
'chat.json': 'bare_os_chat',
|
'chat.json': 'bare_os_chat',
|
||||||
|
meshdrop: 'bare_os_meshdrop',
|
||||||
|
'meshdrop.json': 'bare_os_meshdrop',
|
||||||
process_table: 'bare_os_process_table',
|
process_table: 'bare_os_process_table',
|
||||||
'process_table.json': 'bare_os_process_table',
|
'process_table.json': 'bare_os_process_table',
|
||||||
process_io: 'bare_os_process_io',
|
process_io: 'bare_os_process_io',
|
||||||
@@ -3200,6 +3212,14 @@ export function createVfs(
|
|||||||
file: 'bare_os_chat'
|
file: 'bare_os_chat'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (sub === 'bare_os_meshdrop' || sub === 'bare_os_meshdrop.json') {
|
||||||
|
return {
|
||||||
|
virtualPseudo: true,
|
||||||
|
kind: 'proc',
|
||||||
|
node: 'file',
|
||||||
|
file: 'bare_os_meshdrop'
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
sub === 'bare_os_metrics_prom' ||
|
sub === 'bare_os_metrics_prom' ||
|
||||||
sub === 'bare_os_metrics_prom.json'
|
sub === 'bare_os_metrics_prom.json'
|
||||||
@@ -4737,6 +4757,7 @@ export function createVfs(
|
|||||||
'bare_os_boot_graph.json',
|
'bare_os_boot_graph.json',
|
||||||
'bare_os_boot_budget_summary.json',
|
'bare_os_boot_budget_summary.json',
|
||||||
'bare_os_chat.json',
|
'bare_os_chat.json',
|
||||||
|
'bare_os_meshdrop.json',
|
||||||
'bare_os_initd_graph.json',
|
'bare_os_initd_graph.json',
|
||||||
'bare_os_ipc_backpressure.json',
|
'bare_os_ipc_backpressure.json',
|
||||||
'bare_os_kernel_program.json',
|
'bare_os_kernel_program.json',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import test from 'brittle'
|
import test from 'brittle'
|
||||||
import { createBareOsChatService } from './lib/bare-os-chat-service.js'
|
import { createBareOsChatService } from './lib/bare-os-chat-service.js'
|
||||||
|
import { createBareOsMeshdropService } from './lib/bare-os-meshdrop-service.js'
|
||||||
|
|
||||||
test('chat service snapshot exposes protocol id', async (t) => {
|
test('chat service snapshot exposes protocol id', async (t) => {
|
||||||
const svc = createBareOsChatService({
|
const svc = createBareOsChatService({
|
||||||
@@ -8,3 +9,11 @@ test('chat service snapshot exposes protocol id', async (t) => {
|
|||||||
const snap = svc.snapshotMetrics()
|
const snap = svc.snapshotMetrics()
|
||||||
t.ok(String(snap.protocol || '').includes('bare-os-chat'))
|
t.ok(String(snap.protocol || '').includes('bare-os-chat'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('meshdrop service snapshot exposes protocol id', async (t) => {
|
||||||
|
const svc = createBareOsMeshdropService({
|
||||||
|
env: {}
|
||||||
|
})
|
||||||
|
const snap = svc.snapshotMetrics()
|
||||||
|
t.ok(String(snap.protocol || '').includes('bare-os-meshdrop'))
|
||||||
|
})
|
||||||
|
|||||||
@@ -50,6 +50,17 @@ const preamble = {
|
|||||||
'edit-stream-read.js',
|
'edit-stream-read.js',
|
||||||
'chat-tui.js'
|
'chat-tui.js'
|
||||||
],
|
],
|
||||||
|
swarmtop: [
|
||||||
|
'edit-ansi.js',
|
||||||
|
'edit-key-parse.js',
|
||||||
|
'edit-stream-read.js',
|
||||||
|
'p2p-suite.js',
|
||||||
|
'p2p-suite-tui.js'
|
||||||
|
],
|
||||||
|
meshdrop: ['p2p-suite.js'],
|
||||||
|
taskmesh: ['p2p-suite.js'],
|
||||||
|
peernote: ['p2p-suite.js'],
|
||||||
|
'hypershell-board': ['p2p-suite.js'],
|
||||||
agent: [
|
agent: [
|
||||||
'edit-ansi.js',
|
'edit-ansi.js',
|
||||||
'edit-key-parse.js',
|
'edit-key-parse.js',
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ export const COREUTILS_COMMANDS = [
|
|||||||
'holesail',
|
'holesail',
|
||||||
'hostid',
|
'hostid',
|
||||||
'hostname',
|
'hostname',
|
||||||
|
'hypershell-board',
|
||||||
'iconv',
|
'iconv',
|
||||||
'id',
|
'id',
|
||||||
'install',
|
'install',
|
||||||
@@ -79,6 +80,7 @@ export const COREUTILS_COMMANDS = [
|
|||||||
'ls',
|
'ls',
|
||||||
'man',
|
'man',
|
||||||
'md5sum',
|
'md5sum',
|
||||||
|
'meshdrop',
|
||||||
'mkdir',
|
'mkdir',
|
||||||
'mkfifo',
|
'mkfifo',
|
||||||
'mktemp',
|
'mktemp',
|
||||||
@@ -97,6 +99,7 @@ export const COREUTILS_COMMANDS = [
|
|||||||
'patch',
|
'patch',
|
||||||
'pathchk',
|
'pathchk',
|
||||||
'pathcap-verify',
|
'pathcap-verify',
|
||||||
|
'peernote',
|
||||||
'pr',
|
'pr',
|
||||||
'printenv',
|
'printenv',
|
||||||
'pkg-swarm-index',
|
'pkg-swarm-index',
|
||||||
@@ -127,6 +130,7 @@ export const COREUTILS_COMMANDS = [
|
|||||||
'ssh-keygen',
|
'ssh-keygen',
|
||||||
'sshd',
|
'sshd',
|
||||||
'stat',
|
'stat',
|
||||||
|
'swarmtop',
|
||||||
'sum',
|
'sum',
|
||||||
'sync',
|
'sync',
|
||||||
'tar',
|
'tar',
|
||||||
@@ -144,6 +148,7 @@ export const COREUTILS_COMMANDS = [
|
|||||||
'true',
|
'true',
|
||||||
'tsort',
|
'tsort',
|
||||||
'tty',
|
'tty',
|
||||||
|
'taskmesh',
|
||||||
'ulimit',
|
'ulimit',
|
||||||
'uname',
|
'uname',
|
||||||
'uniq',
|
'uniq',
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
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 : []
|
||||||
|
out.peers = peers.slice(0, 128)
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "hypershell-board",
|
||||||
|
"section": 1,
|
||||||
|
"title": "p2p remote-session board",
|
||||||
|
"synopsis": [
|
||||||
|
"hypershell-board offer-shell LABEL",
|
||||||
|
"hypershell-board offer-copy PATH",
|
||||||
|
"hypershell-board claim SESSION_ID",
|
||||||
|
"hypershell-board close SESSION_ID",
|
||||||
|
"hypershell-board list"
|
||||||
|
],
|
||||||
|
"description": "Publishes and tracks hypershell-style session intents in a p2p event stream (`session.offer`, `session.claim`, `session.close`). This command provides coordination and audit visibility while transport/execution policy remains host-controlled.",
|
||||||
|
"options": [],
|
||||||
|
"keywords": ["p2p", "hypershell", "remote-shell", "sessions"],
|
||||||
|
"examples": [
|
||||||
|
{ "caption": "offer a shell session", "code": "hypershell-board offer-shell ops-maintenance" },
|
||||||
|
{ "caption": "claim an offered session", "code": "hypershell-board claim session-abc123" }
|
||||||
|
],
|
||||||
|
"seeAlso": [
|
||||||
|
{ "name": "chat", "section": 1 },
|
||||||
|
{ "name": "telnet", "section": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "meshdrop",
|
||||||
|
"section": 1,
|
||||||
|
"title": "p2p file inbox and outbox",
|
||||||
|
"synopsis": [
|
||||||
|
"meshdrop offer FILE [--to PEER_HINT]",
|
||||||
|
"meshdrop send-next OFFER_ID [MAX_CHUNKS]",
|
||||||
|
"meshdrop inbox [N]",
|
||||||
|
"meshdrop accept OFFER_ID",
|
||||||
|
"meshdrop fetch OFFER_ID [DEST]"
|
||||||
|
],
|
||||||
|
"description": "Transfers files between booted Bare OS peers using a chunked `bare-p2p-v1` meshdrop protocol carried over the existing p2p chat transport. Transfer lifecycle uses offer/accept/chunk/progress/cancel/complete events with local queue state under `~/.bare/p2p-suite/meshdrop.json`. Host bridge hooks can consume emitted mirror-drive hints.",
|
||||||
|
"options": [],
|
||||||
|
"keywords": ["p2p", "files", "meshdrop", "swarm"],
|
||||||
|
"examples": [
|
||||||
|
{ "caption": "offer a file to peers", "code": "meshdrop offer ./report.txt" },
|
||||||
|
{ "caption": "send more queued chunks", "code": "meshdrop send-next offer-abc123 16" },
|
||||||
|
{ "caption": "accept then inspect transfer progress", "code": "meshdrop accept offer-abc123 && meshdrop status" },
|
||||||
|
{ "caption": "save an accepted transfer", "code": "meshdrop fetch offer-abc123 ./received-report.txt" }
|
||||||
|
],
|
||||||
|
"seeAlso": [
|
||||||
|
{ "name": "chat", "section": 1 },
|
||||||
|
{ "name": "tar", "section": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "peernote",
|
||||||
|
"section": 1,
|
||||||
|
"title": "shared p2p notes feed",
|
||||||
|
"synopsis": [
|
||||||
|
"peernote add TEXT",
|
||||||
|
"peernote list [N]"
|
||||||
|
],
|
||||||
|
"description": "Writes and reads short collaborative notes over the p2p swarm by using `bare-p2p-v1` envelopes carried on the existing chat transport. Useful for operational breadcrumbs across booted systems.",
|
||||||
|
"options": [],
|
||||||
|
"keywords": ["p2p", "notes", "chat", "ops"],
|
||||||
|
"examples": [
|
||||||
|
{ "caption": "add a note", "code": "peernote add rebooted edge node 3 with new profile" },
|
||||||
|
{ "caption": "list recent notes", "code": "peernote list 20" }
|
||||||
|
],
|
||||||
|
"seeAlso": [
|
||||||
|
{ "name": "taskmesh", "section": 1 },
|
||||||
|
{ "name": "chat", "section": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "swarmtop",
|
||||||
|
"section": 1,
|
||||||
|
"title": "p2p fleet dashboard",
|
||||||
|
"synopsis": [
|
||||||
|
"swarmtop",
|
||||||
|
"swarmtop watch",
|
||||||
|
"swarmtop events [N]",
|
||||||
|
"swarmtop ping [LABEL]"
|
||||||
|
],
|
||||||
|
"description": "Shows a fleet-oriented p2p dashboard for booted Bare OS peers. It combines `/proc/bare_os/swarm` snapshots with `bare-p2p-v1` envelopes transported over the existing swarm chat channel. On a TTY with no subcommand it opens a lightweight full-screen view.",
|
||||||
|
"options": [],
|
||||||
|
"keywords": ["p2p", "swarm", "dashboard", "tui"],
|
||||||
|
"examples": [
|
||||||
|
{ "caption": "open full-screen dashboard", "code": "swarmtop" },
|
||||||
|
{ "caption": "emit a fleet ping event", "code": "swarmtop ping maintenance-window" }
|
||||||
|
],
|
||||||
|
"seeAlso": [
|
||||||
|
{ "name": "chat", "section": 1 },
|
||||||
|
{ "name": "baretop", "section": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "taskmesh",
|
||||||
|
"section": 1,
|
||||||
|
"title": "shared p2p task board",
|
||||||
|
"synopsis": [
|
||||||
|
"taskmesh add TEXT",
|
||||||
|
"taskmesh done TASK_ID",
|
||||||
|
"taskmesh list [open|all]"
|
||||||
|
],
|
||||||
|
"description": "Maintains a swarm-shared append-only task stream using `bare-p2p-v1` envelopes. The local task board is reduced from event history (`task.add`, `task.done`) so multiple booted peers can converge without central coordination.",
|
||||||
|
"options": [],
|
||||||
|
"keywords": ["p2p", "tasks", "kanban", "events"],
|
||||||
|
"examples": [
|
||||||
|
{ "caption": "create a shared task", "code": "taskmesh add rotate bootstrap relays" },
|
||||||
|
{ "caption": "show open tasks", "code": "taskmesh list" }
|
||||||
|
],
|
||||||
|
"seeAlso": [
|
||||||
|
{ "name": "peernote", "section": 1 },
|
||||||
|
{ "name": "chat", "section": 1 }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,6 +6,6 @@
|
|||||||
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
|
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
|
"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"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'hypershell-board'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' offer-shell LABEL\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' offer-copy PATH\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' claim SESSION_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' close SESSION_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' list\n' +
|
||||||
|
'P2P hypershell-style session board (intent + audit feed).\n' +
|
||||||
|
'See man hypershell-board.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'offer-shell' || sub === 'offer-copy') {
|
||||||
|
const label = args.slice(1).join(' ').trim()
|
||||||
|
if (!label) {
|
||||||
|
ctx.console.error(argv0 + ': missing label/path')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sessionId = bareP2pId('session')
|
||||||
|
const mode = sub === 'offer-shell' ? 'shell' : 'copy'
|
||||||
|
const r = bareP2pSend(ctx, 'hypershell-board', 'session.offer', {
|
||||||
|
sessionId,
|
||||||
|
mode,
|
||||||
|
label
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log('offered ' + sessionId + ' (' + mode + ')')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'claim' || sub === 'close') {
|
||||||
|
const sessionId = String(args[1] || '').trim()
|
||||||
|
if (!sessionId) {
|
||||||
|
ctx.console.error(argv0 + ': missing SESSION_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const kind = sub === 'claim' ? 'session.claim' : 'session.close'
|
||||||
|
const r = bareP2pSend(ctx, 'hypershell-board', kind, { sessionId })
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log(sub + ' ' + sessionId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'list') {
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'hypershell-board', 2000)
|
||||||
|
/** @type {Map<string, { sessionId: string, mode: string, label: string, state: string, atMs: number }>} */
|
||||||
|
const sessions = new Map()
|
||||||
|
for (const row of rows) {
|
||||||
|
const kind = row.packet?.kind
|
||||||
|
const p = row.packet?.payload
|
||||||
|
if (!p || typeof p !== 'object') continue
|
||||||
|
if (kind === 'session.offer' && typeof p.sessionId === 'string') {
|
||||||
|
sessions.set(p.sessionId, {
|
||||||
|
sessionId: p.sessionId,
|
||||||
|
mode: typeof p.mode === 'string' ? p.mode : 'shell',
|
||||||
|
label: typeof p.label === 'string' ? p.label : '',
|
||||||
|
state: 'open',
|
||||||
|
atMs: row.receivedAtMs
|
||||||
|
})
|
||||||
|
} else if (kind === 'session.claim' && typeof p.sessionId === 'string') {
|
||||||
|
const s = sessions.get(p.sessionId)
|
||||||
|
if (s) s.state = 'claimed'
|
||||||
|
} else if (kind === 'session.close' && typeof p.sessionId === 'string') {
|
||||||
|
const s = sessions.get(p.sessionId)
|
||||||
|
if (s) s.state = 'closed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const s of [...sessions.values()].sort((a, b) => b.atMs - a.atMs)) {
|
||||||
|
ctx.console.log(
|
||||||
|
'[' +
|
||||||
|
s.state +
|
||||||
|
'] ' +
|
||||||
|
s.sessionId +
|
||||||
|
' ' +
|
||||||
|
s.mode +
|
||||||
|
' ' +
|
||||||
|
s.label
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
const BARE_MESHDROP_CHUNK_RAW = 12 * 1024
|
||||||
|
|
||||||
|
function bareMeshdropName(path) {
|
||||||
|
const parts = String(path || '').split('/')
|
||||||
|
return parts[parts.length - 1] || 'file.bin'
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareMeshdropChunkB64Limit() {
|
||||||
|
const raw = BARE_MESHDROP_CHUNK_RAW
|
||||||
|
return Math.ceil((raw * 4) / 3) + 16
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareMeshdropChunkSliceB64(b64, index) {
|
||||||
|
const max = bareMeshdropChunkB64Limit()
|
||||||
|
const from = index * max
|
||||||
|
const to = Math.min(b64.length, from + max)
|
||||||
|
return b64.slice(from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bareMeshdropDbRead(ctx) {
|
||||||
|
const p = bareP2pDataFile(ctx, 'meshdrop')
|
||||||
|
const cur = await bareP2pReadJson(ctx, p)
|
||||||
|
if (cur && typeof cur === 'object') return cur
|
||||||
|
return {
|
||||||
|
schema: 2,
|
||||||
|
outgoing: {},
|
||||||
|
incoming: {},
|
||||||
|
transfers: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bareMeshdropDbWrite(ctx, db) {
|
||||||
|
return bareP2pWriteJson(ctx, bareP2pDataFile(ctx, 'meshdrop'), db)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareMeshdropEnsureTransfer(db, transferId) {
|
||||||
|
if (!db.transfers || typeof db.transfers !== 'object') db.transfers = {}
|
||||||
|
if (!db.transfers[transferId]) {
|
||||||
|
db.transfers[transferId] = {
|
||||||
|
transferId,
|
||||||
|
status: 'new',
|
||||||
|
chunksTotal: 0,
|
||||||
|
chunksReceived: 0,
|
||||||
|
updatedAtMs: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db.transfers[transferId]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'meshdrop'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' offer FILE [--to PEER_HINT]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' send-next OFFER_ID [MAX_CHUNKS]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' inbox [N]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' accept OFFER_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' fetch OFFER_ID [DEST]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' status [TRANSFER_ID]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' cancel TRANSFER_ID\n' +
|
||||||
|
'P2P file inbox/outbox over bare-p2p envelopes on chat transport.\n' +
|
||||||
|
'See man meshdrop.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'offer') {
|
||||||
|
const file = args[1]
|
||||||
|
if (!file) {
|
||||||
|
ctx.console.error(argv0 + ': offer requires FILE')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const b = await ctx.vfs.readFile(file)
|
||||||
|
if (!b) {
|
||||||
|
ctx.console.error(argv0 + ': unable to read ' + file)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const b64 = ctx.b4a.toString(b, 'base64')
|
||||||
|
const toIdx = args.indexOf('--to')
|
||||||
|
const to = toIdx >= 0 ? String(args[toIdx + 1] || '').trim() : ''
|
||||||
|
const offerId = bareP2pId('offer')
|
||||||
|
const transferId = bareP2pId('xfer')
|
||||||
|
const chunkChars = bareMeshdropChunkB64Limit()
|
||||||
|
const chunksTotal = Math.max(1, Math.ceil(b64.length / chunkChars))
|
||||||
|
const firstChunk = bareMeshdropChunkSliceB64(b64, 0)
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
if (!db.outgoing || typeof db.outgoing !== 'object') db.outgoing = {}
|
||||||
|
db.outgoing[offerId] = {
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
file,
|
||||||
|
fileName: bareMeshdropName(file),
|
||||||
|
byteLength: b.byteLength,
|
||||||
|
base64: b64,
|
||||||
|
chunkChars,
|
||||||
|
chunksTotal,
|
||||||
|
sentChunks: firstChunk ? 1 : 0,
|
||||||
|
to,
|
||||||
|
status: 'offered',
|
||||||
|
updatedAtMs: Date.now()
|
||||||
|
}
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, transferId)
|
||||||
|
tr.role = 'sender'
|
||||||
|
tr.offerId = offerId
|
||||||
|
tr.status = 'offered'
|
||||||
|
tr.chunksTotal = chunksTotal
|
||||||
|
tr.chunksReceived = 0
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
|
||||||
|
if (typeof ctx.bareOsEmitMirrorDriveHint === 'function') {
|
||||||
|
ctx.bareOsEmitMirrorDriveHint({
|
||||||
|
app: 'meshdrop',
|
||||||
|
phase: 'offer',
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
fileName: bareMeshdropName(file),
|
||||||
|
byteLength: b.byteLength,
|
||||||
|
chunksTotal
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const r = bareP2pSend(ctx, 'meshdrop', 'offer', {
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
fromPath: file,
|
||||||
|
fileName: bareMeshdropName(file),
|
||||||
|
byteLength: b.byteLength,
|
||||||
|
chunkChars,
|
||||||
|
chunksTotal,
|
||||||
|
firstChunkB64: firstChunk,
|
||||||
|
to
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) {
|
||||||
|
ctx.console.error(argv0 + ': ' + String(r.reason || 'send failed'))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(
|
||||||
|
'offered ' + file + ' as ' + offerId + ' (' + chunksTotal + ' chunks)'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'send-next') {
|
||||||
|
const offerId = String(args[1] || '').trim()
|
||||||
|
if (!offerId) {
|
||||||
|
ctx.console.error(argv0 + ': send-next requires OFFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const maxChunks = Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(64, parseInt(args[2] || '8', 10) || 8)
|
||||||
|
)
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const rec = db?.outgoing?.[offerId]
|
||||||
|
if (!rec) {
|
||||||
|
ctx.console.error(argv0 + ': unknown offer: ' + offerId)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let sent = 0
|
||||||
|
while (sent < maxChunks && rec.sentChunks < rec.chunksTotal) {
|
||||||
|
const idx = rec.sentChunks
|
||||||
|
const chunk = bareMeshdropChunkSliceB64(rec.base64, idx)
|
||||||
|
const r = bareP2pSend(ctx, 'meshdrop', 'chunk', {
|
||||||
|
offerId: rec.offerId,
|
||||||
|
transferId: rec.transferId,
|
||||||
|
index: idx,
|
||||||
|
chunksTotal: rec.chunksTotal,
|
||||||
|
chunkB64: chunk
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) break
|
||||||
|
rec.sentChunks++
|
||||||
|
rec.updatedAtMs = Date.now()
|
||||||
|
sent++
|
||||||
|
}
|
||||||
|
rec.status = rec.sentChunks >= rec.chunksTotal ? 'all-chunks-sent' : 'sending'
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, rec.transferId)
|
||||||
|
tr.status = rec.status
|
||||||
|
tr.updatedAtMs = rec.updatedAtMs
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'progress', {
|
||||||
|
offerId: rec.offerId,
|
||||||
|
transferId: rec.transferId,
|
||||||
|
sentChunks: rec.sentChunks,
|
||||||
|
chunksTotal: rec.chunksTotal
|
||||||
|
})
|
||||||
|
ctx.console.log(
|
||||||
|
'sent ' + sent + ' chunk(s), ' + rec.sentChunks + '/' + rec.chunksTotal
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'inbox') {
|
||||||
|
const n = Math.max(1, Math.min(200, parseInt(args[1] || '20', 10) || 20))
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
const offers = rows.filter((r) => r.packet?.kind === 'offer').slice(-n)
|
||||||
|
for (const row of offers) {
|
||||||
|
const p = row.packet?.payload || {}
|
||||||
|
const offerId = typeof p.offerId === 'string' ? p.offerId : '?'
|
||||||
|
const fileName = typeof p.fileName === 'string' ? p.fileName : '?'
|
||||||
|
const bytes = typeof p.byteLength === 'number' ? p.byteLength : 0
|
||||||
|
const chunks = typeof p.chunksTotal === 'number' ? p.chunksTotal : '?'
|
||||||
|
const from = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
|
ctx.console.log(
|
||||||
|
'[' +
|
||||||
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
|
'] ' +
|
||||||
|
offerId +
|
||||||
|
' ' +
|
||||||
|
fileName +
|
||||||
|
' ' +
|
||||||
|
bytes +
|
||||||
|
'B chunks=' +
|
||||||
|
String(chunks) +
|
||||||
|
' from=' +
|
||||||
|
from
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'accept') {
|
||||||
|
const offerId = String(args[1] || '').trim()
|
||||||
|
if (!offerId) {
|
||||||
|
ctx.console.error(argv0 + ': accept requires OFFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
const offer = rows.findLast(
|
||||||
|
(r) =>
|
||||||
|
r.packet?.kind === 'offer' &&
|
||||||
|
r.packet.payload &&
|
||||||
|
typeof r.packet.payload === 'object' &&
|
||||||
|
r.packet.payload.offerId === offerId
|
||||||
|
)
|
||||||
|
if (!offer) {
|
||||||
|
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const p = offer.packet.payload
|
||||||
|
const transferId =
|
||||||
|
typeof p.transferId === 'string' ? p.transferId : bareP2pId('xfer')
|
||||||
|
const fileName =
|
||||||
|
typeof p.fileName === 'string' ? p.fileName : offerId + '.received'
|
||||||
|
const chunksTotal = typeof p.chunksTotal === 'number' ? p.chunksTotal : 1
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
if (!db.incoming || typeof db.incoming !== 'object') db.incoming = {}
|
||||||
|
db.incoming[offerId] = {
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
fileName,
|
||||||
|
chunksTotal,
|
||||||
|
chunks: {},
|
||||||
|
acceptedAtMs: Date.now(),
|
||||||
|
status: 'accepted'
|
||||||
|
}
|
||||||
|
if (typeof p.firstChunkB64 === 'string' && p.firstChunkB64) {
|
||||||
|
db.incoming[offerId].chunks[0] = p.firstChunkB64
|
||||||
|
}
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, transferId)
|
||||||
|
tr.role = 'receiver'
|
||||||
|
tr.offerId = offerId
|
||||||
|
tr.status = 'accepted'
|
||||||
|
tr.chunksTotal = chunksTotal
|
||||||
|
tr.chunksReceived = Object.keys(db.incoming[offerId].chunks).length
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'accept', { offerId, transferId })
|
||||||
|
ctx.console.log('accepted ' + offerId + ' (transfer ' + transferId + ')')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'fetch') {
|
||||||
|
const offerId = String(args[1] || '').trim()
|
||||||
|
if (!offerId) {
|
||||||
|
ctx.console.error(argv0 + ': fetch requires OFFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
const offer = rows.findLast(
|
||||||
|
(r) =>
|
||||||
|
r.packet?.kind === 'offer' &&
|
||||||
|
r.packet.payload &&
|
||||||
|
typeof r.packet.payload === 'object' &&
|
||||||
|
r.packet.payload.offerId === offerId
|
||||||
|
)
|
||||||
|
if (!offer) {
|
||||||
|
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const p = offer.packet.payload
|
||||||
|
const fileName =
|
||||||
|
typeof p.fileName === 'string' ? p.fileName : offerId + '.bin'
|
||||||
|
const dest = args[2] || fileName
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const incoming = db?.incoming?.[offerId]
|
||||||
|
if (!incoming) {
|
||||||
|
ctx.console.error(
|
||||||
|
argv0 +
|
||||||
|
': offer is not accepted locally; run `' +
|
||||||
|
argv0 +
|
||||||
|
' accept ' +
|
||||||
|
offerId +
|
||||||
|
'` first'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const chunks = incoming.chunks && typeof incoming.chunks === 'object' ? incoming.chunks : {}
|
||||||
|
const total = typeof incoming.chunksTotal === 'number' ? incoming.chunksTotal : 0
|
||||||
|
const pieces = []
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const part = chunks[i]
|
||||||
|
if (typeof part !== 'string' || !part) {
|
||||||
|
ctx.console.error(
|
||||||
|
argv0 +
|
||||||
|
': missing chunk ' +
|
||||||
|
i +
|
||||||
|
'/' +
|
||||||
|
total +
|
||||||
|
' (run `' +
|
||||||
|
argv0 +
|
||||||
|
' status ' +
|
||||||
|
incoming.transferId +
|
||||||
|
'` to inspect)'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pieces.push(part)
|
||||||
|
}
|
||||||
|
const buf = ctx.b4a.from(pieces.join(''), 'base64')
|
||||||
|
await ctx.vfs.writeFile(dest, buf)
|
||||||
|
incoming.status = 'saved'
|
||||||
|
incoming.savedTo = dest
|
||||||
|
incoming.savedAtMs = Date.now()
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, incoming.transferId)
|
||||||
|
tr.status = 'saved'
|
||||||
|
tr.chunksReceived = total
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'complete', {
|
||||||
|
offerId,
|
||||||
|
transferId: incoming.transferId,
|
||||||
|
savedTo: dest
|
||||||
|
})
|
||||||
|
ctx.console.log('saved ' + offerId + ' -> ' + dest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'status') {
|
||||||
|
const transferId = String(args[1] || '').trim()
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
for (const row of rows) {
|
||||||
|
const kind = row.packet?.kind
|
||||||
|
const p = row.packet?.payload
|
||||||
|
if (!p || typeof p !== 'object') continue
|
||||||
|
const id = typeof p.transferId === 'string' ? p.transferId : ''
|
||||||
|
if (transferId && id !== transferId) continue
|
||||||
|
if (kind === 'chunk' && typeof p.offerId === 'string' && typeof p.index === 'number') {
|
||||||
|
const inRec = db?.incoming?.[p.offerId]
|
||||||
|
if (inRec && inRec.status !== 'cancelled' && inRec.status !== 'saved') {
|
||||||
|
if (!inRec.chunks || typeof inRec.chunks !== 'object') inRec.chunks = {}
|
||||||
|
if (typeof p.chunkB64 === 'string' && !inRec.chunks[p.index]) {
|
||||||
|
inRec.chunks[p.index] = p.chunkB64
|
||||||
|
}
|
||||||
|
const got = Object.keys(inRec.chunks).length
|
||||||
|
inRec.status = got >= inRec.chunksTotal ? 'received-all' : 'receiving'
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, inRec.transferId)
|
||||||
|
tr.status = inRec.status
|
||||||
|
tr.chunksTotal = inRec.chunksTotal
|
||||||
|
tr.chunksReceived = got
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kind === 'cancel' && id) {
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, id)
|
||||||
|
tr.status = 'cancelled'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
if (kind === 'progress' && id) {
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, id)
|
||||||
|
if (typeof p.sentChunks === 'number') tr.sentChunks = p.sentChunks
|
||||||
|
if (typeof p.chunksTotal === 'number') tr.chunksTotal = p.chunksTotal
|
||||||
|
tr.status = 'sending'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
if (kind === 'complete' && id) {
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, id)
|
||||||
|
tr.status = 'complete'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
const items = Object.values(db.transfers || {})
|
||||||
|
.filter((x) => !transferId || x.transferId === transferId)
|
||||||
|
.sort((a, b) => (b.updatedAtMs || 0) - (a.updatedAtMs || 0))
|
||||||
|
if (!items.length) {
|
||||||
|
ctx.console.log('no transfers')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const it of items) {
|
||||||
|
ctx.console.log(
|
||||||
|
(it.transferId || '?') +
|
||||||
|
' role=' +
|
||||||
|
String(it.role || '?') +
|
||||||
|
' status=' +
|
||||||
|
String(it.status || '?') +
|
||||||
|
' chunks=' +
|
||||||
|
String(it.chunksReceived || 0) +
|
||||||
|
'/' +
|
||||||
|
String(it.chunksTotal || 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'cancel') {
|
||||||
|
const transferId = String(args[1] || '').trim()
|
||||||
|
if (!transferId) {
|
||||||
|
ctx.console.error(argv0 + ': cancel requires TRANSFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, transferId)
|
||||||
|
tr.status = 'cancelled'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'cancel', { transferId })
|
||||||
|
ctx.console.log('cancelled ' + transferId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'peernote'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' add TEXT\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' list [N]\n' +
|
||||||
|
'Shared p2p note stream over bare-p2p envelopes.\n' +
|
||||||
|
'See man peernote.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'add') {
|
||||||
|
const text = args.slice(1).join(' ').trim()
|
||||||
|
if (!text) {
|
||||||
|
ctx.console.error(argv0 + ': add requires text')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const r = bareP2pSend(ctx, 'peernote', 'note.add', {
|
||||||
|
noteId: bareP2pId('note'),
|
||||||
|
text
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (sub === 'list') {
|
||||||
|
const n = Math.max(1, Math.min(300, parseInt(args[1] || '30', 10) || 30))
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'peernote', 2000)
|
||||||
|
.filter((r) => r.packet?.kind === 'note.add')
|
||||||
|
.slice(-n)
|
||||||
|
for (const row of rows) {
|
||||||
|
const p = row.packet?.payload || {}
|
||||||
|
const text = typeof p.text === 'string' ? p.text : ''
|
||||||
|
const who = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
|
ctx.console.log('[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + who + ': ' + text)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'swarmtop'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help')) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' [watch | events [N] | ping [LABEL]]\n' +
|
||||||
|
'P2P fleet dashboard over Bare OS chat + /proc swarm data.\n' +
|
||||||
|
'No args on a TTY opens full-screen swarmtop.\n' +
|
||||||
|
'See man swarmtop.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = args[0] || ''
|
||||||
|
if (sub === 'ping') {
|
||||||
|
const label = args.slice(1).join(' ').trim() || 'hello'
|
||||||
|
const r = bareP2pSend(ctx, 'swarmtop', 'ping', {
|
||||||
|
id: bareP2pId('ping'),
|
||||||
|
label
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (sub === 'events') {
|
||||||
|
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '30', 10) || 30))
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'swarmtop', n)
|
||||||
|
for (const row of rows.slice(-n)) {
|
||||||
|
const pkt = row.packet || {}
|
||||||
|
const payload = pkt.payload && typeof pkt.payload === 'object' ? pkt.payload : {}
|
||||||
|
ctx.console.log(
|
||||||
|
'[' +
|
||||||
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
|
'] ' +
|
||||||
|
String(pkt.kind || '?') +
|
||||||
|
' ' +
|
||||||
|
JSON.stringify(payload)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdin = ctx.replStdin
|
||||||
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
|
if (!sub && isTTY) {
|
||||||
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
|
title: 'swarmtop',
|
||||||
|
subtitle: 'fleet p2p dashboard · q quit · r refresh',
|
||||||
|
renderLines: async () => {
|
||||||
|
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
|
const meshdropRaw =
|
||||||
|
ctx.vfs && typeof ctx.vfs.readFile === 'function'
|
||||||
|
? await ctx.vfs.readFile('/proc/bare_os/meshdrop.json')
|
||||||
|
: null
|
||||||
|
let meshdrop = null
|
||||||
|
try {
|
||||||
|
meshdrop = meshdropRaw ? JSON.parse(ctx.b4a.toString(meshdropRaw)) : null
|
||||||
|
} catch {
|
||||||
|
meshdrop = null
|
||||||
|
}
|
||||||
|
const events = bareP2pCollectFromHistory(ctx, 'swarmtop', 64).slice(-8)
|
||||||
|
/** @type {string[]} */
|
||||||
|
const lines = []
|
||||||
|
lines.push('peerCount: ' + String(snap.peerCount ?? '?'))
|
||||||
|
lines.push('topicCount: ' + String(snap.topicCount ?? '?'))
|
||||||
|
const mdRx =
|
||||||
|
meshdrop && typeof meshdrop === 'object'
|
||||||
|
? meshdrop.protomuxMeshdropRxTotal
|
||||||
|
: null
|
||||||
|
const mdTx =
|
||||||
|
meshdrop &&
|
||||||
|
typeof meshdrop === 'object' &&
|
||||||
|
meshdrop.metrics &&
|
||||||
|
typeof meshdrop.metrics === 'object'
|
||||||
|
? meshdrop.metrics.txEnvelope
|
||||||
|
: null
|
||||||
|
lines.push(
|
||||||
|
'meshdrop rx/tx: ' +
|
||||||
|
String(mdRx != null ? mdRx : '?') +
|
||||||
|
'/' +
|
||||||
|
String(mdTx != null ? mdTx : '?')
|
||||||
|
)
|
||||||
|
lines.push('')
|
||||||
|
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)) {
|
||||||
|
const key =
|
||||||
|
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
|
||||||
|
? p.remotePublicKey.slice(0, 16)
|
||||||
|
: '?'
|
||||||
|
const state =
|
||||||
|
p && typeof p === 'object' && typeof p.state === 'string'
|
||||||
|
? p.state
|
||||||
|
: 'connected'
|
||||||
|
lines.push(' ' + key + ' ' + state)
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('Recent events:')
|
||||||
|
if (!events.length) lines.push(' (none)')
|
||||||
|
for (const ev of events) {
|
||||||
|
lines.push(
|
||||||
|
' [' +
|
||||||
|
bareP2pFmtClock(ev.receivedAtMs) +
|
||||||
|
'] ' +
|
||||||
|
String(ev.packet?.kind || '?')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'watch') {
|
||||||
|
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
|
ctx.console.log(JSON.stringify(snap, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'taskmesh'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' add TEXT\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' done TASK_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' list [open|all]\n' +
|
||||||
|
'P2P task board over append-only bare-p2p events.\n' +
|
||||||
|
'See man taskmesh.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'add') {
|
||||||
|
const text = args.slice(1).join(' ').trim()
|
||||||
|
if (!text) {
|
||||||
|
ctx.console.error(argv0 + ': add requires text')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const taskId = bareP2pId('task')
|
||||||
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.add', {
|
||||||
|
taskId,
|
||||||
|
text
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log('added ' + taskId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'done') {
|
||||||
|
const taskId = String(args[1] || '').trim()
|
||||||
|
if (!taskId) {
|
||||||
|
ctx.console.error(argv0 + ': done requires TASK_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.done', { taskId })
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log('completed ' + taskId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'list') {
|
||||||
|
const mode = args[1] === 'all' ? 'all' : 'open'
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'taskmesh', 2000)
|
||||||
|
/** @type {Map<string, { taskId: string, text: string, done: boolean, atMs: number }>} */
|
||||||
|
const board = new Map()
|
||||||
|
for (const row of rows) {
|
||||||
|
const kind = row.packet?.kind
|
||||||
|
const p = row.packet?.payload
|
||||||
|
if (!p || typeof p !== 'object') continue
|
||||||
|
if (kind === 'task.add' && typeof p.taskId === 'string') {
|
||||||
|
board.set(p.taskId, {
|
||||||
|
taskId: p.taskId,
|
||||||
|
text: typeof p.text === 'string' ? p.text : '',
|
||||||
|
done: false,
|
||||||
|
atMs: row.receivedAtMs
|
||||||
|
})
|
||||||
|
} else if (kind === 'task.done' && typeof p.taskId === 'string') {
|
||||||
|
const cur = board.get(p.taskId)
|
||||||
|
if (cur) cur.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const all = [...board.values()].sort((a, b) => a.atMs - b.atMs)
|
||||||
|
for (const t of all) {
|
||||||
|
if (mode !== 'all' && t.done) continue
|
||||||
|
ctx.console.log((t.done ? '[x] ' : '[ ] ') + t.taskId + ' ' + t.text)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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, '..')
|
||||||
|
const kernelBin = join(__dirname, '../../../kernel/bin')
|
||||||
|
|
||||||
|
async function read(p) {
|
||||||
|
return readFile(p, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
test('p2p suite commands include shared envelope helper in kernel bins', async (t) => {
|
||||||
|
const names = [
|
||||||
|
'swarmtop',
|
||||||
|
'meshdrop',
|
||||||
|
'taskmesh',
|
||||||
|
'peernote',
|
||||||
|
'hypershell-board'
|
||||||
|
]
|
||||||
|
for (const name of names) {
|
||||||
|
const src = await read(join(kernelBin, name))
|
||||||
|
t.ok(src.includes('BARE_P2P_SUITE_PREFIX'), name + ' includes p2p preamble')
|
||||||
|
t.ok(src.includes('async function run(ctx, argv)'), name + ' includes run()')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('p2p suite man pages exist', async (t) => {
|
||||||
|
const pages = [
|
||||||
|
'swarmtop',
|
||||||
|
'meshdrop',
|
||||||
|
'taskmesh',
|
||||||
|
'peernote',
|
||||||
|
'hypershell-board'
|
||||||
|
]
|
||||||
|
for (const page of pages) {
|
||||||
|
const body = await read(join(coreutilsRoot, 'man/pages', page + '.json'))
|
||||||
|
t.ok(body.includes('"name": "' + page + '"'), page + ' man page present')
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -9,6 +9,8 @@ export const PROTOCOL_APP_CHANNEL_NAME = 'bare-os-app-v1'
|
|||||||
export const PROTOCOL_CAP_CHANNEL_NAME = 'bare-os-cap-v1'
|
export const PROTOCOL_CAP_CHANNEL_NAME = 'bare-os-cap-v1'
|
||||||
/** Global swarm chat channel (paired by default; opt out with `BARE_OS_PROTOMUX_CHAT_CHANNEL=0`). */
|
/** Global swarm chat channel (paired by default; opt out with `BARE_OS_PROTOMUX_CHAT_CHANNEL=0`). */
|
||||||
export const PROTOCOL_CHAT_CHANNEL_NAME = 'bare-os-chat-v1'
|
export const PROTOCOL_CHAT_CHANNEL_NAME = 'bare-os-chat-v1'
|
||||||
|
/** Meshdrop control/data channel (paired by default; opt out with `BARE_OS_PROTOMUX_MESHDROP_CHANNEL=0`). */
|
||||||
|
export const PROTOCOL_MESHDROP_CHANNEL_NAME = 'bare-os-meshdrop-v1'
|
||||||
export const BLOCK_SIZE = 512
|
export const BLOCK_SIZE = 512
|
||||||
export const MBR_MAGIC = b4a.from('BIOS')
|
export const MBR_MAGIC = b4a.from('BIOS')
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export {
|
|||||||
PROTOCOL_APP_CHANNEL_NAME,
|
PROTOCOL_APP_CHANNEL_NAME,
|
||||||
PROTOCOL_CAP_CHANNEL_NAME,
|
PROTOCOL_CAP_CHANNEL_NAME,
|
||||||
PROTOCOL_CHAT_CHANNEL_NAME,
|
PROTOCOL_CHAT_CHANNEL_NAME,
|
||||||
|
PROTOCOL_MESHDROP_CHANNEL_NAME,
|
||||||
TOPIC_STRING,
|
TOPIC_STRING,
|
||||||
BLOCK_SIZE,
|
BLOCK_SIZE,
|
||||||
MBR_MAGIC,
|
MBR_MAGIC,
|
||||||
@@ -14,6 +15,8 @@ export {
|
|||||||
export { setupSeedChannel } from './lib/channel.js'
|
export { setupSeedChannel } from './lib/channel.js'
|
||||||
export { setupBareOsChatChannel } from './lib/chat-channel.js'
|
export { setupBareOsChatChannel } from './lib/chat-channel.js'
|
||||||
export { bareOsProtMuxChatChannelEnabled } from './lib/chat-env.js'
|
export { bareOsProtMuxChatChannelEnabled } from './lib/chat-env.js'
|
||||||
|
export { setupBareOsMeshdropChannel } from './lib/meshdrop-channel.js'
|
||||||
|
export { bareOsProtMuxMeshdropChannelEnabled } from './lib/meshdrop-env.js'
|
||||||
export {
|
export {
|
||||||
BARE_OS_CHAT_WIRE_SCHEMA_VERSION,
|
BARE_OS_CHAT_WIRE_SCHEMA_VERSION,
|
||||||
BARE_OS_CHAT_EVT_TEXT,
|
BARE_OS_CHAT_EVT_TEXT,
|
||||||
@@ -25,6 +28,10 @@ export {
|
|||||||
msgChatEventEncoding,
|
msgChatEventEncoding,
|
||||||
msgChatControlEncoding
|
msgChatControlEncoding
|
||||||
} from './lib/chat-messages.js'
|
} from './lib/chat-messages.js'
|
||||||
|
export {
|
||||||
|
BARE_OS_MESHDROP_WIRE_SCHEMA_VERSION,
|
||||||
|
msgMeshdropEnvelopeEncoding
|
||||||
|
} from './lib/meshdrop-messages.js'
|
||||||
export {
|
export {
|
||||||
BARE_OS_SEED_RPC_METHODS,
|
BARE_OS_SEED_RPC_METHODS,
|
||||||
BARE_OS_SEED_RPC_METHOD_SHORT_NAMES,
|
BARE_OS_SEED_RPC_METHOD_SHORT_NAMES,
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { PROTOCOL_MESHDROP_CHANNEL_NAME } from '../constants.js'
|
||||||
|
import { msgMeshdropEnvelopeEncoding } from './meshdrop-messages.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dedicated Protomux channel for meshdrop control/chunk frames.
|
||||||
|
*
|
||||||
|
* @param {import('protomux').Protomux} mux
|
||||||
|
* @param {{
|
||||||
|
* onEnvelope?: (m: Record<string, unknown>, ch: import('protomux').Channel) => void
|
||||||
|
* onChannelOpened?: (ch: import('protomux').Channel, mux: import('protomux').Protomux) => void
|
||||||
|
* }} [handlers]
|
||||||
|
*/
|
||||||
|
export function setupBareOsMeshdropChannel(mux, handlers = {}) {
|
||||||
|
const chan = mux.createChannel({
|
||||||
|
protocol: PROTOCOL_MESHDROP_CHANNEL_NAME,
|
||||||
|
unique: true
|
||||||
|
})
|
||||||
|
if (!chan) return
|
||||||
|
chan.addMessage({
|
||||||
|
encoding: msgMeshdropEnvelopeEncoding,
|
||||||
|
onmessage: (m, ch) =>
|
||||||
|
handlers.onEnvelope ? handlers.onEnvelope(m, ch) : undefined
|
||||||
|
})
|
||||||
|
chan.open()
|
||||||
|
if (typeof handlers.onChannelOpened === 'function') {
|
||||||
|
handlers.onChannelOpened(chan, mux)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Default-on meshdrop channel toggle.
|
||||||
|
* Disable only with explicit falsey env:
|
||||||
|
* - BARE_OS_PROTOMUX_MESHDROP_CHANNEL=0
|
||||||
|
* - BARE_OS_PROTOMUX_MESHDROP_CHANNEL=false
|
||||||
|
*
|
||||||
|
* @param {Record<string, string | undefined>} [env]
|
||||||
|
*/
|
||||||
|
export function bareOsProtMuxMeshdropChannelEnabled(
|
||||||
|
env = /** @type {Record<string, string | undefined>} */ (globalThis.process?.env || {})
|
||||||
|
) {
|
||||||
|
const raw = String(env.BARE_OS_PROTOMUX_MESHDROP_CHANNEL ?? '').trim().toLowerCase()
|
||||||
|
if (!raw) return true
|
||||||
|
if (raw === '0' || raw === 'false' || raw === 'off' || raw === 'no') return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import c from 'compact-encoding'
|
||||||
|
|
||||||
|
export const BARE_OS_MESHDROP_WIRE_SCHEMA_VERSION = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compact JSON payload wrapper. The booter validates and caps shape.
|
||||||
|
*/
|
||||||
|
export const msgMeshdropEnvelopeEncoding = {
|
||||||
|
preencode(state, m) {
|
||||||
|
c.string.preencode(state, JSON.stringify(m || {}))
|
||||||
|
},
|
||||||
|
encode(state, m) {
|
||||||
|
c.string.encode(state, JSON.stringify(m || {}))
|
||||||
|
},
|
||||||
|
decode(state) {
|
||||||
|
const t = c.string.decode(state)
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(String(t || ''))
|
||||||
|
return obj && typeof obj === 'object' ? obj : {}
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,10 @@
|
|||||||
"./bare-os-pear-multisig-shape.js": "./lib/bare-os-pear-multisig-shape.js",
|
"./bare-os-pear-multisig-shape.js": "./lib/bare-os-pear-multisig-shape.js",
|
||||||
"./chat-channel.js": "./lib/chat-channel.js",
|
"./chat-channel.js": "./lib/chat-channel.js",
|
||||||
"./chat-messages.js": "./lib/chat-messages.js",
|
"./chat-messages.js": "./lib/chat-messages.js",
|
||||||
"./chat-env.js": "./lib/chat-env.js"
|
"./chat-env.js": "./lib/chat-env.js",
|
||||||
|
"./meshdrop-channel.js": "./lib/meshdrop-channel.js",
|
||||||
|
"./meshdrop-messages.js": "./lib/meshdrop-messages.js",
|
||||||
|
"./meshdrop-env.js": "./lib/meshdrop-env.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "brittle-bare test.js",
|
"test": "brittle-bare test.js",
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ function bareOsEmitRaw(ctx, chunk) {
|
|||||||
return false
|
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 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 mkdir mkfifo mktemp mount mv nano nice nl nohup nproc numfmt od oidc-publish openssl openssl paste patch pathcap-verify pathchk pear-runtime-matrix 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 sync systemctl tac tail tar tar 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 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"
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
|
'Bare OS — default user: guest | shell builtins: alias, barerc, cd, command, export, exit, login, logout, readonly, type, umask, unalias, unset, : | /bin: ' +
|
||||||
|
|||||||
@@ -0,0 +1,406 @@
|
|||||||
|
/* 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 : []
|
||||||
|
out.peers = peers.slice(0, 128)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'hypershell-board'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' offer-shell LABEL\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' offer-copy PATH\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' claim SESSION_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' close SESSION_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' list\n' +
|
||||||
|
'P2P hypershell-style session board (intent + audit feed).\n' +
|
||||||
|
'See man hypershell-board.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'offer-shell' || sub === 'offer-copy') {
|
||||||
|
const label = args.slice(1).join(' ').trim()
|
||||||
|
if (!label) {
|
||||||
|
ctx.console.error(argv0 + ': missing label/path')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sessionId = bareP2pId('session')
|
||||||
|
const mode = sub === 'offer-shell' ? 'shell' : 'copy'
|
||||||
|
const r = bareP2pSend(ctx, 'hypershell-board', 'session.offer', {
|
||||||
|
sessionId,
|
||||||
|
mode,
|
||||||
|
label
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log('offered ' + sessionId + ' (' + mode + ')')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'claim' || sub === 'close') {
|
||||||
|
const sessionId = String(args[1] || '').trim()
|
||||||
|
if (!sessionId) {
|
||||||
|
ctx.console.error(argv0 + ': missing SESSION_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const kind = sub === 'claim' ? 'session.claim' : 'session.close'
|
||||||
|
const r = bareP2pSend(ctx, 'hypershell-board', kind, { sessionId })
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log(sub + ' ' + sessionId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'list') {
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'hypershell-board', 2000)
|
||||||
|
/** @type {Map<string, { sessionId: string, mode: string, label: string, state: string, atMs: number }>} */
|
||||||
|
const sessions = new Map()
|
||||||
|
for (const row of rows) {
|
||||||
|
const kind = row.packet?.kind
|
||||||
|
const p = row.packet?.payload
|
||||||
|
if (!p || typeof p !== 'object') continue
|
||||||
|
if (kind === 'session.offer' && typeof p.sessionId === 'string') {
|
||||||
|
sessions.set(p.sessionId, {
|
||||||
|
sessionId: p.sessionId,
|
||||||
|
mode: typeof p.mode === 'string' ? p.mode : 'shell',
|
||||||
|
label: typeof p.label === 'string' ? p.label : '',
|
||||||
|
state: 'open',
|
||||||
|
atMs: row.receivedAtMs
|
||||||
|
})
|
||||||
|
} else if (kind === 'session.claim' && typeof p.sessionId === 'string') {
|
||||||
|
const s = sessions.get(p.sessionId)
|
||||||
|
if (s) s.state = 'claimed'
|
||||||
|
} else if (kind === 'session.close' && typeof p.sessionId === 'string') {
|
||||||
|
const s = sessions.get(p.sessionId)
|
||||||
|
if (s) s.state = 'closed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const s of [...sessions.values()].sort((a, b) => b.atMs - a.atMs)) {
|
||||||
|
ctx.console.log(
|
||||||
|
'[' +
|
||||||
|
s.state +
|
||||||
|
'] ' +
|
||||||
|
s.sessionId +
|
||||||
|
' ' +
|
||||||
|
s.mode +
|
||||||
|
' ' +
|
||||||
|
s.label
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,766 @@
|
|||||||
|
/* 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 : []
|
||||||
|
out.peers = peers.slice(0, 128)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const BARE_MESHDROP_CHUNK_RAW = 12 * 1024
|
||||||
|
|
||||||
|
function bareMeshdropName(path) {
|
||||||
|
const parts = String(path || '').split('/')
|
||||||
|
return parts[parts.length - 1] || 'file.bin'
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareMeshdropChunkB64Limit() {
|
||||||
|
const raw = BARE_MESHDROP_CHUNK_RAW
|
||||||
|
return Math.ceil((raw * 4) / 3) + 16
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareMeshdropChunkSliceB64(b64, index) {
|
||||||
|
const max = bareMeshdropChunkB64Limit()
|
||||||
|
const from = index * max
|
||||||
|
const to = Math.min(b64.length, from + max)
|
||||||
|
return b64.slice(from, to)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bareMeshdropDbRead(ctx) {
|
||||||
|
const p = bareP2pDataFile(ctx, 'meshdrop')
|
||||||
|
const cur = await bareP2pReadJson(ctx, p)
|
||||||
|
if (cur && typeof cur === 'object') return cur
|
||||||
|
return {
|
||||||
|
schema: 2,
|
||||||
|
outgoing: {},
|
||||||
|
incoming: {},
|
||||||
|
transfers: {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bareMeshdropDbWrite(ctx, db) {
|
||||||
|
return bareP2pWriteJson(ctx, bareP2pDataFile(ctx, 'meshdrop'), db)
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareMeshdropEnsureTransfer(db, transferId) {
|
||||||
|
if (!db.transfers || typeof db.transfers !== 'object') db.transfers = {}
|
||||||
|
if (!db.transfers[transferId]) {
|
||||||
|
db.transfers[transferId] = {
|
||||||
|
transferId,
|
||||||
|
status: 'new',
|
||||||
|
chunksTotal: 0,
|
||||||
|
chunksReceived: 0,
|
||||||
|
updatedAtMs: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return db.transfers[transferId]
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'meshdrop'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' offer FILE [--to PEER_HINT]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' send-next OFFER_ID [MAX_CHUNKS]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' inbox [N]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' accept OFFER_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' fetch OFFER_ID [DEST]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' status [TRANSFER_ID]\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' cancel TRANSFER_ID\n' +
|
||||||
|
'P2P file inbox/outbox over bare-p2p envelopes on chat transport.\n' +
|
||||||
|
'See man meshdrop.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'offer') {
|
||||||
|
const file = args[1]
|
||||||
|
if (!file) {
|
||||||
|
ctx.console.error(argv0 + ': offer requires FILE')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const b = await ctx.vfs.readFile(file)
|
||||||
|
if (!b) {
|
||||||
|
ctx.console.error(argv0 + ': unable to read ' + file)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const b64 = ctx.b4a.toString(b, 'base64')
|
||||||
|
const toIdx = args.indexOf('--to')
|
||||||
|
const to = toIdx >= 0 ? String(args[toIdx + 1] || '').trim() : ''
|
||||||
|
const offerId = bareP2pId('offer')
|
||||||
|
const transferId = bareP2pId('xfer')
|
||||||
|
const chunkChars = bareMeshdropChunkB64Limit()
|
||||||
|
const chunksTotal = Math.max(1, Math.ceil(b64.length / chunkChars))
|
||||||
|
const firstChunk = bareMeshdropChunkSliceB64(b64, 0)
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
if (!db.outgoing || typeof db.outgoing !== 'object') db.outgoing = {}
|
||||||
|
db.outgoing[offerId] = {
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
file,
|
||||||
|
fileName: bareMeshdropName(file),
|
||||||
|
byteLength: b.byteLength,
|
||||||
|
base64: b64,
|
||||||
|
chunkChars,
|
||||||
|
chunksTotal,
|
||||||
|
sentChunks: firstChunk ? 1 : 0,
|
||||||
|
to,
|
||||||
|
status: 'offered',
|
||||||
|
updatedAtMs: Date.now()
|
||||||
|
}
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, transferId)
|
||||||
|
tr.role = 'sender'
|
||||||
|
tr.offerId = offerId
|
||||||
|
tr.status = 'offered'
|
||||||
|
tr.chunksTotal = chunksTotal
|
||||||
|
tr.chunksReceived = 0
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
|
||||||
|
if (typeof ctx.bareOsEmitMirrorDriveHint === 'function') {
|
||||||
|
ctx.bareOsEmitMirrorDriveHint({
|
||||||
|
app: 'meshdrop',
|
||||||
|
phase: 'offer',
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
fileName: bareMeshdropName(file),
|
||||||
|
byteLength: b.byteLength,
|
||||||
|
chunksTotal
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const r = bareP2pSend(ctx, 'meshdrop', 'offer', {
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
fromPath: file,
|
||||||
|
fileName: bareMeshdropName(file),
|
||||||
|
byteLength: b.byteLength,
|
||||||
|
chunkChars,
|
||||||
|
chunksTotal,
|
||||||
|
firstChunkB64: firstChunk,
|
||||||
|
to
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) {
|
||||||
|
ctx.console.error(argv0 + ': ' + String(r.reason || 'send failed'))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(
|
||||||
|
'offered ' + file + ' as ' + offerId + ' (' + chunksTotal + ' chunks)'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'send-next') {
|
||||||
|
const offerId = String(args[1] || '').trim()
|
||||||
|
if (!offerId) {
|
||||||
|
ctx.console.error(argv0 + ': send-next requires OFFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const maxChunks = Math.max(
|
||||||
|
1,
|
||||||
|
Math.min(64, parseInt(args[2] || '8', 10) || 8)
|
||||||
|
)
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const rec = db?.outgoing?.[offerId]
|
||||||
|
if (!rec) {
|
||||||
|
ctx.console.error(argv0 + ': unknown offer: ' + offerId)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let sent = 0
|
||||||
|
while (sent < maxChunks && rec.sentChunks < rec.chunksTotal) {
|
||||||
|
const idx = rec.sentChunks
|
||||||
|
const chunk = bareMeshdropChunkSliceB64(rec.base64, idx)
|
||||||
|
const r = bareP2pSend(ctx, 'meshdrop', 'chunk', {
|
||||||
|
offerId: rec.offerId,
|
||||||
|
transferId: rec.transferId,
|
||||||
|
index: idx,
|
||||||
|
chunksTotal: rec.chunksTotal,
|
||||||
|
chunkB64: chunk
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) break
|
||||||
|
rec.sentChunks++
|
||||||
|
rec.updatedAtMs = Date.now()
|
||||||
|
sent++
|
||||||
|
}
|
||||||
|
rec.status = rec.sentChunks >= rec.chunksTotal ? 'all-chunks-sent' : 'sending'
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, rec.transferId)
|
||||||
|
tr.status = rec.status
|
||||||
|
tr.updatedAtMs = rec.updatedAtMs
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'progress', {
|
||||||
|
offerId: rec.offerId,
|
||||||
|
transferId: rec.transferId,
|
||||||
|
sentChunks: rec.sentChunks,
|
||||||
|
chunksTotal: rec.chunksTotal
|
||||||
|
})
|
||||||
|
ctx.console.log(
|
||||||
|
'sent ' + sent + ' chunk(s), ' + rec.sentChunks + '/' + rec.chunksTotal
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'inbox') {
|
||||||
|
const n = Math.max(1, Math.min(200, parseInt(args[1] || '20', 10) || 20))
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
const offers = rows.filter((r) => r.packet?.kind === 'offer').slice(-n)
|
||||||
|
for (const row of offers) {
|
||||||
|
const p = row.packet?.payload || {}
|
||||||
|
const offerId = typeof p.offerId === 'string' ? p.offerId : '?'
|
||||||
|
const fileName = typeof p.fileName === 'string' ? p.fileName : '?'
|
||||||
|
const bytes = typeof p.byteLength === 'number' ? p.byteLength : 0
|
||||||
|
const chunks = typeof p.chunksTotal === 'number' ? p.chunksTotal : '?'
|
||||||
|
const from = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
|
ctx.console.log(
|
||||||
|
'[' +
|
||||||
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
|
'] ' +
|
||||||
|
offerId +
|
||||||
|
' ' +
|
||||||
|
fileName +
|
||||||
|
' ' +
|
||||||
|
bytes +
|
||||||
|
'B chunks=' +
|
||||||
|
String(chunks) +
|
||||||
|
' from=' +
|
||||||
|
from
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'accept') {
|
||||||
|
const offerId = String(args[1] || '').trim()
|
||||||
|
if (!offerId) {
|
||||||
|
ctx.console.error(argv0 + ': accept requires OFFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
const offer = rows.findLast(
|
||||||
|
(r) =>
|
||||||
|
r.packet?.kind === 'offer' &&
|
||||||
|
r.packet.payload &&
|
||||||
|
typeof r.packet.payload === 'object' &&
|
||||||
|
r.packet.payload.offerId === offerId
|
||||||
|
)
|
||||||
|
if (!offer) {
|
||||||
|
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const p = offer.packet.payload
|
||||||
|
const transferId =
|
||||||
|
typeof p.transferId === 'string' ? p.transferId : bareP2pId('xfer')
|
||||||
|
const fileName =
|
||||||
|
typeof p.fileName === 'string' ? p.fileName : offerId + '.received'
|
||||||
|
const chunksTotal = typeof p.chunksTotal === 'number' ? p.chunksTotal : 1
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
if (!db.incoming || typeof db.incoming !== 'object') db.incoming = {}
|
||||||
|
db.incoming[offerId] = {
|
||||||
|
offerId,
|
||||||
|
transferId,
|
||||||
|
fileName,
|
||||||
|
chunksTotal,
|
||||||
|
chunks: {},
|
||||||
|
acceptedAtMs: Date.now(),
|
||||||
|
status: 'accepted'
|
||||||
|
}
|
||||||
|
if (typeof p.firstChunkB64 === 'string' && p.firstChunkB64) {
|
||||||
|
db.incoming[offerId].chunks[0] = p.firstChunkB64
|
||||||
|
}
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, transferId)
|
||||||
|
tr.role = 'receiver'
|
||||||
|
tr.offerId = offerId
|
||||||
|
tr.status = 'accepted'
|
||||||
|
tr.chunksTotal = chunksTotal
|
||||||
|
tr.chunksReceived = Object.keys(db.incoming[offerId].chunks).length
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'accept', { offerId, transferId })
|
||||||
|
ctx.console.log('accepted ' + offerId + ' (transfer ' + transferId + ')')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'fetch') {
|
||||||
|
const offerId = String(args[1] || '').trim()
|
||||||
|
if (!offerId) {
|
||||||
|
ctx.console.error(argv0 + ': fetch requires OFFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
const offer = rows.findLast(
|
||||||
|
(r) =>
|
||||||
|
r.packet?.kind === 'offer' &&
|
||||||
|
r.packet.payload &&
|
||||||
|
typeof r.packet.payload === 'object' &&
|
||||||
|
r.packet.payload.offerId === offerId
|
||||||
|
)
|
||||||
|
if (!offer) {
|
||||||
|
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const p = offer.packet.payload
|
||||||
|
const fileName =
|
||||||
|
typeof p.fileName === 'string' ? p.fileName : offerId + '.bin'
|
||||||
|
const dest = args[2] || fileName
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const incoming = db?.incoming?.[offerId]
|
||||||
|
if (!incoming) {
|
||||||
|
ctx.console.error(
|
||||||
|
argv0 +
|
||||||
|
': offer is not accepted locally; run `' +
|
||||||
|
argv0 +
|
||||||
|
' accept ' +
|
||||||
|
offerId +
|
||||||
|
'` first'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const chunks = incoming.chunks && typeof incoming.chunks === 'object' ? incoming.chunks : {}
|
||||||
|
const total = typeof incoming.chunksTotal === 'number' ? incoming.chunksTotal : 0
|
||||||
|
const pieces = []
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const part = chunks[i]
|
||||||
|
if (typeof part !== 'string' || !part) {
|
||||||
|
ctx.console.error(
|
||||||
|
argv0 +
|
||||||
|
': missing chunk ' +
|
||||||
|
i +
|
||||||
|
'/' +
|
||||||
|
total +
|
||||||
|
' (run `' +
|
||||||
|
argv0 +
|
||||||
|
' status ' +
|
||||||
|
incoming.transferId +
|
||||||
|
'` to inspect)'
|
||||||
|
)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pieces.push(part)
|
||||||
|
}
|
||||||
|
const buf = ctx.b4a.from(pieces.join(''), 'base64')
|
||||||
|
await ctx.vfs.writeFile(dest, buf)
|
||||||
|
incoming.status = 'saved'
|
||||||
|
incoming.savedTo = dest
|
||||||
|
incoming.savedAtMs = Date.now()
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, incoming.transferId)
|
||||||
|
tr.status = 'saved'
|
||||||
|
tr.chunksReceived = total
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'complete', {
|
||||||
|
offerId,
|
||||||
|
transferId: incoming.transferId,
|
||||||
|
savedTo: dest
|
||||||
|
})
|
||||||
|
ctx.console.log('saved ' + offerId + ' -> ' + dest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'status') {
|
||||||
|
const transferId = String(args[1] || '').trim()
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
|
for (const row of rows) {
|
||||||
|
const kind = row.packet?.kind
|
||||||
|
const p = row.packet?.payload
|
||||||
|
if (!p || typeof p !== 'object') continue
|
||||||
|
const id = typeof p.transferId === 'string' ? p.transferId : ''
|
||||||
|
if (transferId && id !== transferId) continue
|
||||||
|
if (kind === 'chunk' && typeof p.offerId === 'string' && typeof p.index === 'number') {
|
||||||
|
const inRec = db?.incoming?.[p.offerId]
|
||||||
|
if (inRec && inRec.status !== 'cancelled' && inRec.status !== 'saved') {
|
||||||
|
if (!inRec.chunks || typeof inRec.chunks !== 'object') inRec.chunks = {}
|
||||||
|
if (typeof p.chunkB64 === 'string' && !inRec.chunks[p.index]) {
|
||||||
|
inRec.chunks[p.index] = p.chunkB64
|
||||||
|
}
|
||||||
|
const got = Object.keys(inRec.chunks).length
|
||||||
|
inRec.status = got >= inRec.chunksTotal ? 'received-all' : 'receiving'
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, inRec.transferId)
|
||||||
|
tr.status = inRec.status
|
||||||
|
tr.chunksTotal = inRec.chunksTotal
|
||||||
|
tr.chunksReceived = got
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kind === 'cancel' && id) {
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, id)
|
||||||
|
tr.status = 'cancelled'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
if (kind === 'progress' && id) {
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, id)
|
||||||
|
if (typeof p.sentChunks === 'number') tr.sentChunks = p.sentChunks
|
||||||
|
if (typeof p.chunksTotal === 'number') tr.chunksTotal = p.chunksTotal
|
||||||
|
tr.status = 'sending'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
if (kind === 'complete' && id) {
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, id)
|
||||||
|
tr.status = 'complete'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
const items = Object.values(db.transfers || {})
|
||||||
|
.filter((x) => !transferId || x.transferId === transferId)
|
||||||
|
.sort((a, b) => (b.updatedAtMs || 0) - (a.updatedAtMs || 0))
|
||||||
|
if (!items.length) {
|
||||||
|
ctx.console.log('no transfers')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const it of items) {
|
||||||
|
ctx.console.log(
|
||||||
|
(it.transferId || '?') +
|
||||||
|
' role=' +
|
||||||
|
String(it.role || '?') +
|
||||||
|
' status=' +
|
||||||
|
String(it.status || '?') +
|
||||||
|
' chunks=' +
|
||||||
|
String(it.chunksReceived || 0) +
|
||||||
|
'/' +
|
||||||
|
String(it.chunksTotal || 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'cancel') {
|
||||||
|
const transferId = String(args[1] || '').trim()
|
||||||
|
if (!transferId) {
|
||||||
|
ctx.console.error(argv0 + ': cancel requires TRANSFER_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
|
const tr = bareMeshdropEnsureTransfer(db, transferId)
|
||||||
|
tr.status = 'cancelled'
|
||||||
|
tr.updatedAtMs = Date.now()
|
||||||
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
|
bareP2pSend(ctx, 'meshdrop', 'cancel', { transferId })
|
||||||
|
ctx.console.log('cancelled ' + transferId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
/* 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 : []
|
||||||
|
out.peers = peers.slice(0, 128)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'peernote'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' add TEXT\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' list [N]\n' +
|
||||||
|
'Shared p2p note stream over bare-p2p envelopes.\n' +
|
||||||
|
'See man peernote.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'add') {
|
||||||
|
const text = args.slice(1).join(' ').trim()
|
||||||
|
if (!text) {
|
||||||
|
ctx.console.error(argv0 + ': add requires text')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const r = bareP2pSend(ctx, 'peernote', 'note.add', {
|
||||||
|
noteId: bareP2pId('note'),
|
||||||
|
text
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (sub === 'list') {
|
||||||
|
const n = Math.max(1, Math.min(300, parseInt(args[1] || '30', 10) || 30))
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'peernote', 2000)
|
||||||
|
.filter((r) => r.packet?.kind === 'note.add')
|
||||||
|
.slice(-n)
|
||||||
|
for (const row of rows) {
|
||||||
|
const p = row.packet?.payload || {}
|
||||||
|
const text = typeof p.text === 'string' ? p.text : ''
|
||||||
|
const who = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
|
ctx.console.log('[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + who + ': ' + text)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,387 @@
|
|||||||
|
/* 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 : []
|
||||||
|
out.peers = peers.slice(0, 128)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run(ctx, argv) {
|
||||||
|
const argv0 = argv[0] || 'taskmesh'
|
||||||
|
const args = argv.slice(1)
|
||||||
|
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
||||||
|
ctx.console.log(
|
||||||
|
'usage: ' +
|
||||||
|
argv0 +
|
||||||
|
' add TEXT\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' done TASK_ID\n' +
|
||||||
|
' ' +
|
||||||
|
argv0 +
|
||||||
|
' list [open|all]\n' +
|
||||||
|
'P2P task board over append-only bare-p2p events.\n' +
|
||||||
|
'See man taskmesh.'
|
||||||
|
)
|
||||||
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = args[0]
|
||||||
|
if (sub === 'add') {
|
||||||
|
const text = args.slice(1).join(' ').trim()
|
||||||
|
if (!text) {
|
||||||
|
ctx.console.error(argv0 + ': add requires text')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const taskId = bareP2pId('task')
|
||||||
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.add', {
|
||||||
|
taskId,
|
||||||
|
text
|
||||||
|
})
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log('added ' + taskId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'done') {
|
||||||
|
const taskId = String(args[1] || '').trim()
|
||||||
|
if (!taskId) {
|
||||||
|
ctx.console.error(argv0 + ': done requires TASK_ID')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.done', { taskId })
|
||||||
|
if (r && r.ok === false) ctx.exitCode = 1
|
||||||
|
else ctx.console.log('completed ' + taskId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'list') {
|
||||||
|
const mode = args[1] === 'all' ? 'all' : 'open'
|
||||||
|
const rows = bareP2pCollectFromHistory(ctx, 'taskmesh', 2000)
|
||||||
|
/** @type {Map<string, { taskId: string, text: string, done: boolean, atMs: number }>} */
|
||||||
|
const board = new Map()
|
||||||
|
for (const row of rows) {
|
||||||
|
const kind = row.packet?.kind
|
||||||
|
const p = row.packet?.payload
|
||||||
|
if (!p || typeof p !== 'object') continue
|
||||||
|
if (kind === 'task.add' && typeof p.taskId === 'string') {
|
||||||
|
board.set(p.taskId, {
|
||||||
|
taskId: p.taskId,
|
||||||
|
text: typeof p.text === 'string' ? p.text : '',
|
||||||
|
done: false,
|
||||||
|
atMs: row.receivedAtMs
|
||||||
|
})
|
||||||
|
} else if (kind === 'task.done' && typeof p.taskId === 'string') {
|
||||||
|
const cur = board.get(p.taskId)
|
||||||
|
if (cur) cur.done = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const all = [...board.values()].sort((a, b) => a.atMs - b.atMs)
|
||||||
|
for (const t of all) {
|
||||||
|
if (mode !== 'all' && t.done) continue
|
||||||
|
ctx.console.log((t.done ? '[x] ' : '[ ] ') + t.taskId + ' ' + t.text)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.console.error(argv0 + ': unsupported subcommand')
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema": 2,
|
"schema": 2,
|
||||||
"profileId": "bare-os-posix-like",
|
"profileId": "bare-os-posix-like",
|
||||||
"generatedAt": "2026-04-26T10:19:42.583Z",
|
"generatedAt": "2026-04-26T11:47:34.024Z",
|
||||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||||
"commandIndex": [
|
"commandIndex": [
|
||||||
{
|
{
|
||||||
@@ -216,6 +216,10 @@
|
|||||||
"name": "hostname",
|
"name": "hostname",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "hypershell-board",
|
||||||
|
"tier": "tier1_bin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "iconv",
|
"name": "iconv",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
@@ -308,6 +312,10 @@
|
|||||||
"name": "md5sum",
|
"name": "md5sum",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "meshdrop",
|
||||||
|
"tier": "tier1_bin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "mkdir",
|
"name": "mkdir",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
@@ -380,6 +388,10 @@
|
|||||||
"name": "pathchk",
|
"name": "pathchk",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "peernote",
|
||||||
|
"tier": "tier1_bin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "pkg-swarm-index",
|
"name": "pkg-swarm-index",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
@@ -504,6 +516,10 @@
|
|||||||
"name": "sum",
|
"name": "sum",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "swarmtop",
|
||||||
|
"tier": "tier1_bin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "sync",
|
"name": "sync",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
@@ -520,6 +536,10 @@
|
|||||||
"name": "tar",
|
"name": "tar",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "taskmesh",
|
||||||
|
"tier": "tier1_bin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "tee",
|
"name": "tee",
|
||||||
"tier": "tier1_bin"
|
"tier": "tier1_bin"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"atMs": 1777198782582,
|
"atMs": 1777204054023,
|
||||||
"commands": [
|
"commands": [
|
||||||
"agent",
|
"agent",
|
||||||
"arch",
|
"arch",
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"holesail",
|
"holesail",
|
||||||
"hostid",
|
"hostid",
|
||||||
"hostname",
|
"hostname",
|
||||||
|
"hypershell-board",
|
||||||
"iconv",
|
"iconv",
|
||||||
"id",
|
"id",
|
||||||
"install",
|
"install",
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
"ls",
|
"ls",
|
||||||
"man",
|
"man",
|
||||||
"md5sum",
|
"md5sum",
|
||||||
|
"meshdrop",
|
||||||
"mkdir",
|
"mkdir",
|
||||||
"mkfifo",
|
"mkfifo",
|
||||||
"mktemp",
|
"mktemp",
|
||||||
@@ -96,6 +98,7 @@
|
|||||||
"patch",
|
"patch",
|
||||||
"pathcap-verify",
|
"pathcap-verify",
|
||||||
"pathchk",
|
"pathchk",
|
||||||
|
"peernote",
|
||||||
"pkg-swarm-index",
|
"pkg-swarm-index",
|
||||||
"pr",
|
"pr",
|
||||||
"printenv",
|
"printenv",
|
||||||
@@ -127,10 +130,12 @@
|
|||||||
"sshd",
|
"sshd",
|
||||||
"stat",
|
"stat",
|
||||||
"sum",
|
"sum",
|
||||||
|
"swarmtop",
|
||||||
"sync",
|
"sync",
|
||||||
"tac",
|
"tac",
|
||||||
"tail",
|
"tail",
|
||||||
"tar",
|
"tar",
|
||||||
|
"taskmesh",
|
||||||
"tee",
|
"tee",
|
||||||
"telnet",
|
"telnet",
|
||||||
"test",
|
"test",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user