Updates to tools
This commit is contained in:
+121
-8
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,17 +438,22 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'dhtscan'
|
const argv0 = argv[0] || 'dhtscan'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
'\n' +
|
'Print compact DHT scan summary from /proc/bare_os/dht_scan.json.',
|
||||||
'Print compact DHT scan summary from /proc/bare_os/dht_scan.json.\n' +
|
argv0 + ' [--summary] [--json]',
|
||||||
'See man dhtscan.'
|
['--summary', '--json'],
|
||||||
|
['dhttop', 'swarmdoctor']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
||||||
ctx.console.log(JSON.stringify(dht, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: dht.peers ?? null, firewalled: dht?.dhtStatus?.firewalled ?? null } : dht, opt)
|
||||||
|
bareP2pMaybeNext(ctx, opt, 'dhttop')
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-5
@@ -570,6 +570,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -920,18 +1028,25 @@ async function bareP2pRunSimpleTui(ctx, opts) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'dhttop'
|
const argv0 = argv[0] || 'dhttop'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' + argv0 + '\n' + 'Live DHT dashboard TUI.\n' + 'See man dhttop.'
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'Live DHT dashboard TUI.',
|
||||||
|
argv0 + ' [--summary] [--json]',
|
||||||
|
['--summary', '--json'],
|
||||||
|
['dhtscan', 'swarmdoctor']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
||||||
ctx.console.log(JSON.stringify(dht, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: dht.peers ?? null, firewalled: dht?.dhtStatus?.firewalled ?? null } : dht, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
@@ -945,6 +1060,7 @@ async function run(ctx, argv) {
|
|||||||
lines.push('firewalled: ' + String(dht.dhtStatus?.firewalled ?? '?'))
|
lines.push('firewalled: ' + String(dht.dhtStatus?.firewalled ?? '?'))
|
||||||
lines.push('randomized: ' + String(dht.dhtStatus?.randomized ?? '?'))
|
lines.push('randomized: ' + String(dht.dhtStatus?.randomized ?? '?'))
|
||||||
lines.push('bootstrap: ' + String(dht.bootstrapHint || '(default)'))
|
lines.push('bootstrap: ' + String(dht.bootstrapHint || '(default)'))
|
||||||
|
lines.push('last update age ms: ' + String(Math.max(0, Date.now() - Number(dht.atMs || Date.now()))))
|
||||||
lines.push('')
|
lines.push('')
|
||||||
lines.push('raw:')
|
lines.push('raw:')
|
||||||
lines.push(JSON.stringify(dht))
|
lines.push(JSON.stringify(dht))
|
||||||
|
|||||||
+120
-9
@@ -570,6 +570,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -920,22 +1028,25 @@ async function bareP2pRunSimpleTui(ctx, opts) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'holepunch-view'
|
const argv0 = argv[0] || 'holepunch-view'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
'\n' +
|
'NAT and holepunch summary dashboard.',
|
||||||
'NAT and holepunch summary dashboard.\n' +
|
argv0 + ' [--summary] [--json]',
|
||||||
'See man holepunch-view.'
|
['--summary', '--json'],
|
||||||
|
['routeview', 'dhtscan']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
const h = await bareP2pReadProcJson(ctx, '/proc/bare_os/holepunch_summary.json')
|
const h = await bareP2pReadProcJson(ctx, '/proc/bare_os/holepunch_summary.json')
|
||||||
ctx.console.log(JSON.stringify(h, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: h.peers ?? null, firewalled: h.firewalled ?? null } : h, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
|
|||||||
+135
-57
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,26 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'hypershell-board'
|
const argv0 = argv[0] || 'hypershell-board'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' offer-shell LABEL\n' +
|
'P2P hypershell-style session board (intent + audit feed).',
|
||||||
' ' +
|
argv0 + ' offer-shell LABEL | offer-copy PATH | claim SESSION_ID | close SESSION_ID | list',
|
||||||
argv0 +
|
['offer-shell "ops shell"', 'claim session-xxx --yes', 'list --summary'],
|
||||||
' offer-copy PATH\n' +
|
['taskmesh', 'peerctl']
|
||||||
' ' +
|
)
|
||||||
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
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -358,53 +458,40 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'offer-shell' || sub === 'offer-copy') {
|
if (sub === 'offer-shell' || sub === 'offer-copy') {
|
||||||
const label = args.slice(1).join(' ').trim()
|
const label = args.slice(1).join(' ').trim()
|
||||||
if (!label) {
|
if (!label) return bareP2pError(ctx, argv0, 'missing label/path', argv0 + ' --help', opt)
|
||||||
ctx.console.error(argv0 + ': missing label/path')
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sessionId = bareP2pId('session')
|
const sessionId = bareP2pId('session')
|
||||||
const mode = sub === 'offer-shell' ? 'shell' : 'copy'
|
const mode = sub === 'offer-shell' ? 'shell' : 'copy'
|
||||||
const r = bareP2pSend(ctx, 'hypershell-board', 'session.offer', {
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, sessionId, mode, label }, opt)
|
||||||
sessionId,
|
const r = bareP2pSend(ctx, 'hypershell-board', 'session.offer', { sessionId, mode, label })
|
||||||
mode,
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
label
|
else bareP2pPrint(ctx, 'offered ' + sessionId + ' (' + mode + ')', opt)
|
||||||
})
|
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
|
||||||
else ctx.console.log('offered ' + sessionId + ' (' + mode + ')')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'claim' || sub === 'close') {
|
if (sub === 'claim' || sub === 'close') {
|
||||||
const sessionId = String(args[1] || '').trim()
|
const sessionId = String(args[1] || '').trim()
|
||||||
if (!sessionId) {
|
if (!sessionId) return bareP2pError(ctx, argv0, 'missing SESSION_ID', argv0 + ' list', opt)
|
||||||
ctx.console.error(argv0 + ': missing SESSION_ID')
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes for ' + sub, opt)
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const kind = sub === 'claim' ? 'session.claim' : 'session.close'
|
const kind = sub === 'claim' ? 'session.claim' : 'session.close'
|
||||||
const r = bareP2pSend(ctx, 'hypershell-board', kind, { sessionId })
|
const r = bareP2pSend(ctx, 'hypershell-board', kind, { sessionId })
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
else ctx.console.log(sub + ' ' + sessionId)
|
else bareP2pPrint(ctx, sub + ' ' + sessionId, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'hypershell-board', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'hypershell-board', 2000)
|
||||||
/** @type {Map<string, { sessionId: string, mode: string, label: string, state: string, atMs: number }>} */
|
|
||||||
const sessions = new Map()
|
const sessions = new Map()
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const kind = row.packet?.kind
|
const kind = row.packet?.kind
|
||||||
const p = row.packet?.payload
|
const p = row.packet?.payload
|
||||||
if (!p || typeof p !== 'object') continue
|
if (!p || typeof p !== 'object') continue
|
||||||
if (kind === 'session.offer' && typeof p.sessionId === 'string') {
|
if (kind === 'session.offer' && typeof p.sessionId === 'string') {
|
||||||
sessions.set(p.sessionId, {
|
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 })
|
||||||
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') {
|
} else if (kind === 'session.claim' && typeof p.sessionId === 'string') {
|
||||||
const s = sessions.get(p.sessionId)
|
const s = sessions.get(p.sessionId)
|
||||||
if (s) s.state = 'claimed'
|
if (s) s.state = 'claimed'
|
||||||
@@ -413,21 +500,12 @@ async function run(ctx, argv) {
|
|||||||
if (s) s.state = 'closed'
|
if (s) s.state = 'closed'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const s of [...sessions.values()].sort((a, b) => b.atMs - a.atMs)) {
|
const arr=[...sessions.values()].sort((a,b)=>b.atMs-a.atMs)
|
||||||
ctx.console.log(
|
if (opt.summary) return bareP2pPrint(ctx, { sessions: arr.length, open: arr.filter((x)=>x.state==='open').length }, opt)
|
||||||
'[' +
|
for (const s of arr) bareP2pPrint(ctx, '[' + s.state + '] ' + s.sessionId + ' ' + s.mode + ' ' + s.label, opt)
|
||||||
s.state +
|
|
||||||
'] ' +
|
|
||||||
s.sessionId +
|
|
||||||
' ' +
|
|
||||||
s.mode +
|
|
||||||
' ' +
|
|
||||||
s.label
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['offer-shell', 'offer-copy', 'claim', 'close', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
+163
-90
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -379,32 +487,19 @@ function bareMeshdropEnsureTransfer(db, transferId) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'meshdrop'
|
const argv0 = argv[0] || 'meshdrop'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'P2P file inbox/outbox over bare-p2p envelopes on chat transport.',
|
||||||
argv0 +
|
argv0 +
|
||||||
' offer FILE [--to PEER_HINT]\n' +
|
' offer FILE [--to PEER_HINT] | send-next OFFER_ID [MAX_CHUNKS] | inbox [N] | accept OFFER_ID | fetch OFFER_ID [DEST] | status [TRANSFER_ID] | cancel TRANSFER_ID',
|
||||||
' ' +
|
['offer ./file.bin --to peerHint', 'send-next offer-123 8', 'status --summary'],
|
||||||
argv0 +
|
['swarmtop', 'peerctl']
|
||||||
' 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
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -414,16 +509,16 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'offer') {
|
if (sub === 'offer') {
|
||||||
const file = args[1]
|
const file = args[1]
|
||||||
if (!file) {
|
if (!file) {
|
||||||
ctx.console.error(argv0 + ': offer requires FILE')
|
return bareP2pError(ctx, argv0, 'offer requires FILE', argv0 + ' --help', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const b = await ctx.vfs.readFile(file)
|
const b = await ctx.vfs.readFile(file)
|
||||||
if (!b) {
|
if (!b) {
|
||||||
ctx.console.error(argv0 + ': unable to read ' + file)
|
return bareP2pError(ctx, argv0, 'unable to read ' + file, 'check file path', opt)
|
||||||
ctx.exitCode = 1
|
if (opt.dryRun) {
|
||||||
|
bareP2pPrint(ctx, { ok: true, dryRun: true, action: 'offer', file, to }, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const b64 = ctx.b4a.toString(b, 'base64')
|
const b64 = ctx.b4a.toString(b, 'base64')
|
||||||
const toIdx = args.indexOf('--to')
|
const toIdx = args.indexOf('--to')
|
||||||
const to = toIdx >= 0 ? String(args[toIdx + 1] || '').trim() : ''
|
const to = toIdx >= 0 ? String(args[toIdx + 1] || '').trim() : ''
|
||||||
@@ -480,22 +575,17 @@ async function run(ctx, argv) {
|
|||||||
to
|
to
|
||||||
})
|
})
|
||||||
if (r && r.ok === false) {
|
if (r && r.ok === false) {
|
||||||
ctx.console.error(argv0 + ': ' + String(r.reason || 'send failed'))
|
return bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, 'offered ' + file + ' as ' + offerId + ' (' + chunksTotal + ' chunks)', opt)
|
||||||
'offered ' + file + ' as ' + offerId + ' (' + chunksTotal + ' chunks)'
|
bareP2pMaybeNext(ctx, opt, argv0 + ' send-next ' + offerId)
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'send-next') {
|
if (sub === 'send-next') {
|
||||||
const offerId = String(args[1] || '').trim()
|
const offerId = String(args[1] || '').trim()
|
||||||
if (!offerId) {
|
if (!offerId) {
|
||||||
ctx.console.error(argv0 + ': send-next requires OFFER_ID')
|
return bareP2pError(ctx, argv0, 'send-next requires OFFER_ID', argv0 + ' status', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const maxChunks = Math.max(
|
const maxChunks = Math.max(
|
||||||
1,
|
1,
|
||||||
@@ -504,9 +594,7 @@ async function run(ctx, argv) {
|
|||||||
const db = await bareMeshdropDbRead(ctx)
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
const rec = db?.outgoing?.[offerId]
|
const rec = db?.outgoing?.[offerId]
|
||||||
if (!rec) {
|
if (!rec) {
|
||||||
ctx.console.error(argv0 + ': unknown offer: ' + offerId)
|
return bareP2pError(ctx, argv0, 'unknown offer: ' + offerId, argv0 + ' status', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let sent = 0
|
let sent = 0
|
||||||
while (sent < maxChunks && rec.sentChunks < rec.chunksTotal) {
|
while (sent < maxChunks && rec.sentChunks < rec.chunksTotal) {
|
||||||
@@ -535,9 +623,7 @@ async function run(ctx, argv) {
|
|||||||
sentChunks: rec.sentChunks,
|
sentChunks: rec.sentChunks,
|
||||||
chunksTotal: rec.chunksTotal
|
chunksTotal: rec.chunksTotal
|
||||||
})
|
})
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, 'sent ' + sent + ' chunk(s), ' + rec.sentChunks + '/' + rec.chunksTotal, opt)
|
||||||
'sent ' + sent + ' chunk(s), ' + rec.sentChunks + '/' + rec.chunksTotal
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,7 +638,7 @@ async function run(ctx, argv) {
|
|||||||
const bytes = typeof p.byteLength === 'number' ? p.byteLength : 0
|
const bytes = typeof p.byteLength === 'number' ? p.byteLength : 0
|
||||||
const chunks = typeof p.chunksTotal === 'number' ? p.chunksTotal : '?'
|
const chunks = typeof p.chunksTotal === 'number' ? p.chunksTotal : '?'
|
||||||
const from = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
const from = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx,
|
||||||
'[' +
|
'[' +
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
'] ' +
|
'] ' +
|
||||||
@@ -565,7 +651,7 @@ async function run(ctx, argv) {
|
|||||||
String(chunks) +
|
String(chunks) +
|
||||||
' from=' +
|
' from=' +
|
||||||
from
|
from
|
||||||
)
|
, opt)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -573,9 +659,7 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'accept') {
|
if (sub === 'accept') {
|
||||||
const offerId = String(args[1] || '').trim()
|
const offerId = String(args[1] || '').trim()
|
||||||
if (!offerId) {
|
if (!offerId) {
|
||||||
ctx.console.error(argv0 + ': accept requires OFFER_ID')
|
return bareP2pError(ctx, argv0, 'accept requires OFFER_ID', argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
const offer = rows.findLast(
|
const offer = rows.findLast(
|
||||||
@@ -586,9 +670,7 @@ async function run(ctx, argv) {
|
|||||||
r.packet.payload.offerId === offerId
|
r.packet.payload.offerId === offerId
|
||||||
)
|
)
|
||||||
if (!offer) {
|
if (!offer) {
|
||||||
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
return bareP2pError(ctx, argv0, 'offer not found: ' + offerId, argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const p = offer.packet.payload
|
const p = offer.packet.payload
|
||||||
const transferId =
|
const transferId =
|
||||||
@@ -619,16 +701,14 @@ async function run(ctx, argv) {
|
|||||||
tr.updatedAtMs = Date.now()
|
tr.updatedAtMs = Date.now()
|
||||||
await bareMeshdropDbWrite(ctx, db)
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
bareP2pSend(ctx, 'meshdrop', 'accept', { offerId, transferId })
|
bareP2pSend(ctx, 'meshdrop', 'accept', { offerId, transferId })
|
||||||
ctx.console.log('accepted ' + offerId + ' (transfer ' + transferId + ')')
|
bareP2pPrint(ctx, 'accepted ' + offerId + ' (transfer ' + transferId + ')', opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'fetch') {
|
if (sub === 'fetch') {
|
||||||
const offerId = String(args[1] || '').trim()
|
const offerId = String(args[1] || '').trim()
|
||||||
if (!offerId) {
|
if (!offerId) {
|
||||||
ctx.console.error(argv0 + ': fetch requires OFFER_ID')
|
return bareP2pError(ctx, argv0, 'fetch requires OFFER_ID', argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
const offer = rows.findLast(
|
const offer = rows.findLast(
|
||||||
@@ -639,9 +719,7 @@ async function run(ctx, argv) {
|
|||||||
r.packet.payload.offerId === offerId
|
r.packet.payload.offerId === offerId
|
||||||
)
|
)
|
||||||
if (!offer) {
|
if (!offer) {
|
||||||
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
return bareP2pError(ctx, argv0, 'offer not found: ' + offerId, argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const p = offer.packet.payload
|
const p = offer.packet.payload
|
||||||
const fileName =
|
const fileName =
|
||||||
@@ -650,16 +728,7 @@ async function run(ctx, argv) {
|
|||||||
const db = await bareMeshdropDbRead(ctx)
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
const incoming = db?.incoming?.[offerId]
|
const incoming = db?.incoming?.[offerId]
|
||||||
if (!incoming) {
|
if (!incoming) {
|
||||||
ctx.console.error(
|
return bareP2pError(ctx, argv0, 'offer not accepted locally', argv0 + ' accept ' + offerId, opt)
|
||||||
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 chunks = incoming.chunks && typeof incoming.chunks === 'object' ? incoming.chunks : {}
|
||||||
const total = typeof incoming.chunksTotal === 'number' ? incoming.chunksTotal : 0
|
const total = typeof incoming.chunksTotal === 'number' ? incoming.chunksTotal : 0
|
||||||
@@ -667,20 +736,7 @@ async function run(ctx, argv) {
|
|||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
const part = chunks[i]
|
const part = chunks[i]
|
||||||
if (typeof part !== 'string' || !part) {
|
if (typeof part !== 'string' || !part) {
|
||||||
ctx.console.error(
|
return bareP2pError(ctx, argv0, 'missing chunk ' + i + '/' + total, argv0 + ' status ' + incoming.transferId, opt)
|
||||||
argv0 +
|
|
||||||
': missing chunk ' +
|
|
||||||
i +
|
|
||||||
'/' +
|
|
||||||
total +
|
|
||||||
' (run `' +
|
|
||||||
argv0 +
|
|
||||||
' status ' +
|
|
||||||
incoming.transferId +
|
|
||||||
'` to inspect)'
|
|
||||||
)
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
pieces.push(part)
|
pieces.push(part)
|
||||||
}
|
}
|
||||||
@@ -699,7 +755,7 @@ async function run(ctx, argv) {
|
|||||||
transferId: incoming.transferId,
|
transferId: incoming.transferId,
|
||||||
savedTo: dest
|
savedTo: dest
|
||||||
})
|
})
|
||||||
ctx.console.log('saved ' + offerId + ' -> ' + dest)
|
bareP2pPrint(ctx, 'saved ' + offerId + ' -> ' + dest, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,11 +808,23 @@ async function run(ctx, argv) {
|
|||||||
.filter((x) => !transferId || x.transferId === transferId)
|
.filter((x) => !transferId || x.transferId === transferId)
|
||||||
.sort((a, b) => (b.updatedAtMs || 0) - (a.updatedAtMs || 0))
|
.sort((a, b) => (b.updatedAtMs || 0) - (a.updatedAtMs || 0))
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
ctx.console.log('no transfers')
|
bareP2pPrint(ctx, opt.summary ? { transfers: 0 } : 'no transfers', opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (opt.summary) {
|
||||||
|
bareP2pPrint(
|
||||||
|
ctx,
|
||||||
|
{
|
||||||
|
transfers: items.length,
|
||||||
|
active: items.filter((x) => String(x.status || '').includes('send') || String(x.status || '').includes('receiv')).length
|
||||||
|
},
|
||||||
|
opt
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
ctx.console.log(
|
bareP2pPrint(
|
||||||
|
ctx,
|
||||||
(it.transferId || '?') +
|
(it.transferId || '?') +
|
||||||
' role=' +
|
' role=' +
|
||||||
String(it.role || '?') +
|
String(it.role || '?') +
|
||||||
@@ -766,6 +834,8 @@ async function run(ctx, argv) {
|
|||||||
String(it.chunksReceived || 0) +
|
String(it.chunksReceived || 0) +
|
||||||
'/' +
|
'/' +
|
||||||
String(it.chunksTotal || 0)
|
String(it.chunksTotal || 0)
|
||||||
|
,
|
||||||
|
opt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -774,7 +844,10 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'cancel') {
|
if (sub === 'cancel') {
|
||||||
const transferId = String(args[1] || '').trim()
|
const transferId = String(args[1] || '').trim()
|
||||||
if (!transferId) {
|
if (!transferId) {
|
||||||
ctx.console.error(argv0 + ': cancel requires TRANSFER_ID')
|
return bareP2pError(ctx, argv0, 'cancel requires TRANSFER_ID', argv0 + ' status', opt)
|
||||||
|
}
|
||||||
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes to cancel transfer', opt)
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -784,10 +857,10 @@ async function run(ctx, argv) {
|
|||||||
tr.updatedAtMs = Date.now()
|
tr.updatedAtMs = Date.now()
|
||||||
await bareMeshdropDbWrite(ctx, db)
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
bareP2pSend(ctx, 'meshdrop', 'cancel', { transferId })
|
bareP2pSend(ctx, 'meshdrop', 'cancel', { transferId })
|
||||||
ctx.console.log('cancelled ' + transferId)
|
bareP2pPrint(ctx, 'cancelled ' + transferId, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['offer', 'send-next', 'inbox', 'accept', 'fetch', 'status', 'cancel'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-9
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,14 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'p2ping'
|
const argv0 = argv[0] || 'p2ping'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' [LABEL]\n' +
|
'Emit a p2p ping event and print local swarm timing hints.',
|
||||||
'Emit a p2p ping event and print local swarm timing hints.\n' +
|
argv0 + ' [LABEL]',
|
||||||
'See man p2ping.'
|
['hello', '--summary', '--json'],
|
||||||
|
['swarmtop', 'p2ptrace']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -361,6 +473,11 @@ async function run(ctx, argv) {
|
|||||||
localElapsedMs: t1 - t0,
|
localElapsedMs: t1 - t0,
|
||||||
swarmPeers: swarm.peerCount
|
swarmPeers: swarm.peerCount
|
||||||
}
|
}
|
||||||
ctx.console.log(JSON.stringify(out, null, 2))
|
if (opt.summary) {
|
||||||
if (send && send.ok === false) ctx.exitCode = 1
|
bareP2pPrint(ctx, { ok: out.ok, ms: out.localElapsedMs, peers: out.swarmPeers }, opt)
|
||||||
|
} else {
|
||||||
|
bareP2pPrint(ctx, out, opt)
|
||||||
|
}
|
||||||
|
if (send && send.ok === false) bareP2pError(ctx, argv0, String(out.sendReason || 'send failed'), 'swarmdoctor', opt)
|
||||||
|
else bareP2pMaybeNext(ctx, opt, 'p2ptrace p2ping 20')
|
||||||
}
|
}
|
||||||
|
|||||||
+127
-25
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,15 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'p2ptrace'
|
const argv0 = argv[0] || 'p2ptrace'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' [APP] [N]\n' +
|
'Dump recent p2p events as JSON lines.',
|
||||||
'Dump recent p2p events as JSON lines.\n' +
|
argv0 + ' [APP] [N]',
|
||||||
'APP defaults to all known p2p apps.\n' +
|
['swarmtop 20', 'meshdrop 50', '--summary'],
|
||||||
'See man p2ptrace.'
|
['swarmtop', 'peerctl']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -346,30 +457,21 @@ async function run(ctx, argv) {
|
|||||||
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '200', 10) || 200))
|
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '200', 10) || 200))
|
||||||
const apps = app
|
const apps = app
|
||||||
? [app]
|
? [app]
|
||||||
: [
|
: ['swarmtop','meshdrop','taskmesh','peernote','hypershell-board','p2ping','peerdiscover','peerctl']
|
||||||
'swarmtop',
|
|
||||||
'meshdrop',
|
|
||||||
'taskmesh',
|
|
||||||
'peernote',
|
|
||||||
'hypershell-board',
|
|
||||||
'p2ping',
|
|
||||||
'peerdiscover',
|
|
||||||
'peerctl'
|
|
||||||
]
|
|
||||||
const rows = []
|
const rows = []
|
||||||
for (const a of apps) {
|
for (const a of apps) rows.push(...bareP2pCollectFromHistory(ctx, a, n))
|
||||||
rows.push(...bareP2pCollectFromHistory(ctx, a, n))
|
|
||||||
}
|
|
||||||
rows.sort((a, b) => (a.receivedAtMs || 0) - (b.receivedAtMs || 0))
|
rows.sort((a, b) => (a.receivedAtMs || 0) - (b.receivedAtMs || 0))
|
||||||
|
if (opt.summary) {
|
||||||
|
bareP2pPrint(ctx, { apps: apps.length, events: rows.length, lastAgeMs: rows.length ? Math.max(0, Date.now()-Number(rows[rows.length-1].receivedAtMs||Date.now())) : null }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
for (const r of rows.slice(-n)) {
|
for (const r of rows.slice(-n)) {
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, {
|
||||||
JSON.stringify({
|
|
||||||
atMs: r.receivedAtMs || 0,
|
atMs: r.receivedAtMs || 0,
|
||||||
app: r.packet?.app || '?',
|
app: r.packet?.app || '?',
|
||||||
kind: r.packet?.kind || '?',
|
kind: r.packet?.kind || '?',
|
||||||
from: r.displayName || r.fromPeerKey || '',
|
from: r.displayName || r.fromPeerKey || '',
|
||||||
payload: r.packet?.payload || {}
|
payload: r.packet?.payload || {}
|
||||||
})
|
}, { ...opt, json: true })
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+141
-29
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,20 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'peerctl'
|
const argv0 = argv[0] || 'peerctl'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' health\n' +
|
'P2P control plane helper over event envelopes.',
|
||||||
' ' +
|
argv0 + ' health | request ACTION [JSON_PAYLOAD] | history [N]',
|
||||||
argv0 +
|
['health --summary', 'request restart {\"service\":\"swarm\"} --dry-run', 'history 20'],
|
||||||
' request ACTION [JSON_PAYLOAD]\n' +
|
['swarmdoctor', 'p2ptrace']
|
||||||
' ' +
|
)
|
||||||
argv0 +
|
|
||||||
' history [N]\n' +
|
|
||||||
'P2P control plane helper over event envelopes.\n' +
|
|
||||||
'See man peerctl.'
|
|
||||||
)
|
)
|
||||||
if (args.length === 0) ctx.exitCode = 1
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -352,25 +458,19 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'health') {
|
if (sub === 'health') {
|
||||||
const swarm = await bareP2pReadSwarmSnapshot(ctx)
|
const swarm = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
const doctor = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
const doctor = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
||||||
ctx.console.log(
|
const o = {
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
schema: 1,
|
schema: 1,
|
||||||
swarmPeers: swarm.peerCount,
|
swarmPeers: swarm.peerCount,
|
||||||
health: doctor.health || 'unknown',
|
health: doctor.health || 'unknown',
|
||||||
remediation: doctor.remediation || []
|
remediation: doctor.remediation || []
|
||||||
},
|
}
|
||||||
null,
|
bareP2pPrint(ctx, opt.summary ? { health: o.health, peers: o.swarmPeers } : o, opt)
|
||||||
2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'request') {
|
if (sub === 'request') {
|
||||||
const action = String(args[1] || '').trim()
|
const action = String(args[1] || '').trim()
|
||||||
if (!action) {
|
if (!action) {
|
||||||
ctx.console.error(argv0 + ': request requires ACTION')
|
bareP2pError(ctx, argv0, 'request requires ACTION', argv0 + ' --help', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let payload = {}
|
let payload = {}
|
||||||
@@ -378,34 +478,46 @@ async function run(ctx, argv) {
|
|||||||
try {
|
try {
|
||||||
payload = JSON.parse(args.slice(2).join(' '))
|
payload = JSON.parse(args.slice(2).join(' '))
|
||||||
} catch {
|
} catch {
|
||||||
ctx.console.error(argv0 + ': invalid JSON payload')
|
bareP2pError(ctx, argv0, 'invalid JSON payload', argv0 + ' request ACTION {\"key\":\"value\"}', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (opt.dryRun) {
|
||||||
|
bareP2pPrint(ctx, { ok: true, dryRun: true, action, payload }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes to send control request', opt)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
const r = bareP2pSend(ctx, 'peerctl', 'request', {
|
const r = bareP2pSend(ctx, 'peerctl', 'request', {
|
||||||
requestId: bareP2pId('ctl'),
|
requestId: bareP2pId('ctl'),
|
||||||
action,
|
action,
|
||||||
payload
|
payload
|
||||||
})
|
})
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
|
else bareP2pMaybeNext(ctx, opt, argv0 + ' history 20')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'history') {
|
if (sub === 'history') {
|
||||||
const n = Math.max(1, Math.min(1000, parseInt(args[1] || '50', 10) || 50))
|
const n = Math.max(1, Math.min(1000, parseInt(args[1] || '50', 10) || 50))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'peerctl', 2000).slice(-n)
|
const rows = bareP2pCollectFromHistory(ctx, 'peerctl', 2000).slice(-n)
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
ctx.console.log(
|
bareP2pPrint(
|
||||||
|
ctx,
|
||||||
'[' +
|
'[' +
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
'] ' +
|
'] ' +
|
||||||
String(row.packet?.kind || '?') +
|
String(row.packet?.kind || '?') +
|
||||||
' ' +
|
' ' +
|
||||||
JSON.stringify(row.packet?.payload || {})
|
JSON.stringify(row.packet?.payload || {})
|
||||||
|
,
|
||||||
|
opt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['health', 'request', 'history'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
+129
-33
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,17 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'peerdiscover'
|
const argv0 = argv[0] || 'peerdiscover'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' announce SERVICE [META]\n' +
|
'Peer service announce/list over p2p event history.',
|
||||||
' ' +
|
argv0 + ' announce SERVICE [META] | list [N]',
|
||||||
argv0 +
|
['announce filesync "v1"', 'list 20 --summary'],
|
||||||
' list [N]\n' +
|
['p2ptrace', 'swarmmap']
|
||||||
'Peer service announce/list over p2p event history.\n' +
|
)
|
||||||
'See man peerdiscover.'
|
|
||||||
)
|
)
|
||||||
if (args.length === 0) ctx.exitCode = 1
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -348,37 +457,24 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'announce') {
|
if (sub === 'announce') {
|
||||||
const service = String(args[1] || '').trim()
|
const service = String(args[1] || '').trim()
|
||||||
if (!service) {
|
if (!service) return bareP2pError(ctx, argv0, 'announce requires SERVICE', argv0 + ' --help', opt)
|
||||||
ctx.console.error(argv0 + ': announce requires SERVICE')
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const meta = args.slice(2).join(' ').trim()
|
const meta = args.slice(2).join(' ').trim()
|
||||||
const r = bareP2pSend(ctx, 'peerdiscover', 'service.announce', {
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, service, meta }, opt)
|
||||||
service,
|
const r = bareP2pSend(ctx, 'peerdiscover', 'service.announce', { service, meta, id: bareP2pId('svc') })
|
||||||
meta,
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
id: bareP2pId('svc')
|
else bareP2pMaybeNext(ctx, opt, argv0 + ' list 20')
|
||||||
})
|
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const n = Math.max(1, Math.min(500, parseInt(args[1] || '50', 10) || 50))
|
const n = Math.max(1, Math.min(500, parseInt(args[1] || '50', 10) || 50))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'peerdiscover', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'peerdiscover', 2000).filter((r) => r.packet?.kind === 'service.announce').slice(-n)
|
||||||
.filter((r) => r.packet?.kind === 'service.announce')
|
if (opt.summary) return bareP2pPrint(ctx, { services: rows.length }, opt)
|
||||||
.slice(-n)
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const p = row.packet?.payload || {}
|
const p = row.packet?.payload || {}
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, '[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + String(p.service || 'unknown') + (p.meta ? ' ' + String(p.meta) : ''), opt)
|
||||||
'[' +
|
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
|
||||||
'] ' +
|
|
||||||
String(p.service || 'unknown') +
|
|
||||||
(p.meta ? ' ' + String(p.meta) : '')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['announce', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
+129
-26
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,17 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'peernote'
|
const argv0 = argv[0] || 'peernote'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' add TEXT\n' +
|
'Shared p2p note stream over bare-p2p envelopes.',
|
||||||
' ' +
|
argv0 + ' add TEXT | list [N]',
|
||||||
argv0 +
|
['add "hello cluster"', 'list 20 --summary'],
|
||||||
' list [N]\n' +
|
['taskmesh', 'p2ptrace']
|
||||||
'Shared p2p note stream over bare-p2p envelopes.\n' +
|
)
|
||||||
'See man peernote.'
|
|
||||||
)
|
)
|
||||||
if (args.length === 0) ctx.exitCode = 1
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -348,31 +457,25 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'add') {
|
if (sub === 'add') {
|
||||||
const text = args.slice(1).join(' ').trim()
|
const text = args.slice(1).join(' ').trim()
|
||||||
if (!text) {
|
if (!text) return bareP2pError(ctx, argv0, 'add requires text', argv0 + ' add "text"', opt)
|
||||||
ctx.console.error(argv0 + ': add requires text')
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, text }, opt)
|
||||||
ctx.exitCode = 1
|
const r = bareP2pSend(ctx, 'peernote', 'note.add', { noteId: bareP2pId('note'), text })
|
||||||
return
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
}
|
else bareP2pMaybeNext(ctx, opt, argv0 + ' list 20')
|
||||||
const r = bareP2pSend(ctx, 'peernote', 'note.add', {
|
|
||||||
noteId: bareP2pId('note'),
|
|
||||||
text
|
|
||||||
})
|
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const n = Math.max(1, Math.min(300, parseInt(args[1] || '30', 10) || 30))
|
const n = Math.max(1, Math.min(300, parseInt(args[1] || '30', 10) || 30))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'peernote', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'peernote', 2000).filter((r) => r.packet?.kind === 'note.add').slice(-n)
|
||||||
.filter((r) => r.packet?.kind === 'note.add')
|
if (opt.summary) return bareP2pPrint(ctx, { notes: rows.length }, opt)
|
||||||
.slice(-n)
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const p = row.packet?.payload || {}
|
const p = row.packet?.payload || {}
|
||||||
const text = typeof p.text === 'string' ? p.text : ''
|
const text = typeof p.text === 'string' ? p.text : ''
|
||||||
const who = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
const who = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
ctx.console.log('[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + who + ': ' + text)
|
bareP2pPrint(ctx, '[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + who + ': ' + text, opt)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['add', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
+121
-5
@@ -570,6 +570,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -920,18 +1028,26 @@ async function bareP2pRunSimpleTui(ctx, opts) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'routeview'
|
const argv0 = argv[0] || 'routeview'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' + argv0 + '\n' + 'Route and relay visibility TUI.\n' + 'See man routeview.'
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'Route and relay visibility TUI.',
|
||||||
|
argv0 + ' [--summary] [--json]',
|
||||||
|
['--summary', '--json'],
|
||||||
|
['holepunch-view', 'swarmdoctor']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
const r = await bareP2pReadProcJson(ctx, '/proc/bare_os/route_summary.json')
|
const r = await bareP2pReadProcJson(ctx, '/proc/bare_os/route_summary.json')
|
||||||
ctx.console.log(JSON.stringify(r, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: r.peers ?? null, direct: r.directLikely ?? null, relay: r.relayLikely ?? null } : r, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
|
|||||||
+120
-8
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,20 +438,24 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'swarmdoctor'
|
const argv0 = argv[0] || 'swarmdoctor'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
'\n' +
|
'Print swarm diagnosis from /proc/bare_os/swarm_doctor.json.',
|
||||||
'Print swarm diagnosis from /proc/bare_os/swarm_doctor.json.\n' +
|
argv0 + ' [--summary] [--json]',
|
||||||
'See man swarmdoctor.'
|
['--summary', '--json'],
|
||||||
|
['swarmtop', 'peerctl']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const doc = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
const doc = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
||||||
ctx.console.log(JSON.stringify(doc, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { health: doc.health || 'unknown', findings: (doc.findings && doc.findings.length) || 0 } : doc, opt)
|
||||||
if (doc && typeof doc === 'object' && doc.health && doc.health !== 'ok') {
|
if (doc && typeof doc === 'object' && doc.health && doc.health !== 'ok') {
|
||||||
|
if (!opt.quiet) ctx.console.error('hint: check routeview and holepunch-view for network posture')
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+120
-5
@@ -570,6 +570,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -920,18 +1028,25 @@ async function bareP2pRunSimpleTui(ctx, opts) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'swarmmap'
|
const argv0 = argv[0] || 'swarmmap'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' + argv0 + '\n' + 'Swarm topology map TUI.\n' + 'See man swarmmap.'
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'Swarm topology map TUI.',
|
||||||
|
argv0 + ' [--summary] [--json]',
|
||||||
|
['--summary', '--json'],
|
||||||
|
['swarmtop', 'peerdiscover']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
ctx.console.log(JSON.stringify(snap, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: snap.peerCount, topics: snap.topicCount } : snap, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
|
|||||||
+167
-20
@@ -570,6 +570,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -920,36 +1028,46 @@ async function bareP2pRunSimpleTui(ctx, opts) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'swarmtop'
|
const argv0 = argv[0] || 'swarmtop'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' [watch | events [N] | ping [LABEL]]\n' +
|
'P2P fleet dashboard over Bare OS chat + /proc swarm data.',
|
||||||
'P2P fleet dashboard over Bare OS chat + /proc swarm data.\n' +
|
argv0 + ' [watch | events [N] | ping [LABEL]]',
|
||||||
'No args on a TTY opens full-screen swarmtop.\n' +
|
['ping hello', 'events 20', 'watch --json'],
|
||||||
'See man swarmtop.'
|
['swarmdoctor', 'swarmmap', 'p2ptrace']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
bareP2pFirstRunHint(ctx, 'swarmtop', "run 'swarmdoctor' when peer rows look incomplete")
|
||||||
|
|
||||||
const sub = args[0] || ''
|
const sub = args[0] || ''
|
||||||
if (sub === 'ping') {
|
if (sub === 'ping') {
|
||||||
const label = args.slice(1).join(' ').trim() || 'hello'
|
const label = args.slice(1).join(' ').trim() || 'hello'
|
||||||
|
if (opt.dryRun) {
|
||||||
|
bareP2pPrint(ctx, { ok: true, dryRun: true, action: 'ping', label }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
const r = bareP2pSend(ctx, 'swarmtop', 'ping', {
|
const r = bareP2pSend(ctx, 'swarmtop', 'ping', {
|
||||||
id: bareP2pId('ping'),
|
id: bareP2pId('ping'),
|
||||||
label
|
label
|
||||||
})
|
})
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
|
else bareP2pMaybeNext(ctx, opt, 'swarmtop events 20')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'events') {
|
if (sub === 'events') {
|
||||||
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '30', 10) || 30))
|
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '30', 10) || 30))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'swarmtop', n)
|
const rows = bareP2pCollectFromHistory(ctx, 'swarmtop', n)
|
||||||
|
const out = []
|
||||||
for (const row of rows.slice(-n)) {
|
for (const row of rows.slice(-n)) {
|
||||||
const pkt = row.packet || {}
|
const pkt = row.packet || {}
|
||||||
const payload = pkt.payload && typeof pkt.payload === 'object' ? pkt.payload : {}
|
const payload = pkt.payload && typeof pkt.payload === 'object' ? pkt.payload : {}
|
||||||
ctx.console.log(
|
out.push(
|
||||||
'[' +
|
'[' +
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
'] ' +
|
'] ' +
|
||||||
@@ -958,6 +1076,12 @@ async function run(ctx, argv) {
|
|||||||
JSON.stringify(payload)
|
JSON.stringify(payload)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (!out.length) out.push('(none)')
|
||||||
|
if (opt.summary) {
|
||||||
|
bareP2pPrint(ctx, { app: 'swarmtop', events: out.length }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const ln of out) bareP2pPrint(ctx, ln, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,6 +1129,7 @@ async function run(ctx, argv) {
|
|||||||
lines.push('Peers:')
|
lines.push('Peers:')
|
||||||
const peers = Array.isArray(snap.peers) ? snap.peers : []
|
const peers = Array.isArray(snap.peers) ? snap.peers : []
|
||||||
if (!peers.length) lines.push(' (no peers reported)')
|
if (!peers.length) lines.push(' (no peers reported)')
|
||||||
|
let unknownCount = 0
|
||||||
for (const p of peers.slice(0, 12)) {
|
for (const p of peers.slice(0, 12)) {
|
||||||
const key =
|
const key =
|
||||||
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
|
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
|
||||||
@@ -1014,12 +1139,19 @@ async function run(ctx, argv) {
|
|||||||
p && typeof p === 'object' && typeof p.state === 'string'
|
p && typeof p === 'object' && typeof p.state === 'string'
|
||||||
? p.state
|
? p.state
|
||||||
: 'connected'
|
: 'connected'
|
||||||
const addr =
|
const endpointObj =
|
||||||
p && typeof p === 'object' && p.endpoint && typeof p.endpoint === 'object'
|
p && typeof p === 'object' && p.endpoint && typeof p.endpoint === 'object'
|
||||||
? String(p.endpoint.address || p.remoteAddress || '?') +
|
? p.endpoint
|
||||||
':' +
|
: null
|
||||||
String(p.endpoint.port || p.remotePort || '?')
|
const addrRaw = String(
|
||||||
: String(p?.remoteAddress || '?') + ':' + String(p?.remotePort || '?')
|
(endpointObj && endpointObj.address) ||
|
||||||
|
(p && typeof p === 'object' ? p.remoteAddress || '' : '')
|
||||||
|
).trim()
|
||||||
|
const portRaw = String(
|
||||||
|
(endpointObj && endpointObj.port) ||
|
||||||
|
(p && typeof p === 'object' ? p.remotePort || '' : '')
|
||||||
|
).trim()
|
||||||
|
const addr = addrRaw && portRaw ? addrRaw + ':' + portRaw : 'unavailable'
|
||||||
const osHint =
|
const osHint =
|
||||||
p &&
|
p &&
|
||||||
typeof p === 'object' &&
|
typeof p === 'object' &&
|
||||||
@@ -1028,12 +1160,27 @@ async function run(ctx, argv) {
|
|||||||
p.peerOs.osHint
|
p.peerOs.osHint
|
||||||
? String(p.peerOs.osHint)
|
? String(p.peerOs.osHint)
|
||||||
: 'unknown'
|
: 'unknown'
|
||||||
|
const osConfidence = osHint === 'unknown' ? 'unknown' : 'reported'
|
||||||
|
if (addr === 'unavailable' || osHint === 'unknown') unknownCount++
|
||||||
lines.push(' pubkey: ' + key)
|
lines.push(' pubkey: ' + key)
|
||||||
lines.push(' state=' + state + ' endpoint=' + addr + ' os=' + osHint)
|
lines.push(
|
||||||
|
' state=' +
|
||||||
|
state +
|
||||||
|
' endpoint=' +
|
||||||
|
addr +
|
||||||
|
' os=' +
|
||||||
|
osHint +
|
||||||
|
' osConfidence=' +
|
||||||
|
osConfidence
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
lines.push(' peerData diagnostics: incomplete=' + unknownCount + '/' + peers.length)
|
||||||
lines.push('')
|
lines.push('')
|
||||||
lines.push('Recent events:')
|
lines.push('Recent events:')
|
||||||
if (!events.length) lines.push(' (none)')
|
if (!events.length) {
|
||||||
|
lines.push(' (none)')
|
||||||
|
lines.push(' event ingest: no app events seen yet; try r, then swarmtop ping hello')
|
||||||
|
}
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
lines.push(
|
lines.push(
|
||||||
' [' +
|
' [' +
|
||||||
@@ -1050,10 +1197,10 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
if (sub === 'watch') {
|
if (sub === 'watch') {
|
||||||
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
ctx.console.log(JSON.stringify(snap, null, 2))
|
bareP2pPrint(ctx, snap, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['watch', 'events', 'ping'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
+137
-38
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,20 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'taskmesh'
|
const argv0 = argv[0] || 'taskmesh'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' add TEXT\n' +
|
'P2P task board over append-only bare-p2p events.',
|
||||||
' ' +
|
argv0 + ' add TEXT | done TASK_ID | list [open|all]',
|
||||||
argv0 +
|
['add "ship release"', 'list all', 'done task-abc'],
|
||||||
' done TASK_ID\n' +
|
['peernote', 'p2ptrace']
|
||||||
' ' +
|
)
|
||||||
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
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -352,63 +458,56 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'add') {
|
if (sub === 'add') {
|
||||||
const text = args.slice(1).join(' ').trim()
|
const text = args.slice(1).join(' ').trim()
|
||||||
if (!text) {
|
if (!text) return bareP2pError(ctx, argv0, 'add requires text', argv0 + ' add "text"', opt)
|
||||||
ctx.console.error(argv0 + ': add requires text')
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const taskId = bareP2pId('task')
|
const taskId = bareP2pId('task')
|
||||||
const r = bareP2pSend(ctx, 'taskmesh', 'task.add', {
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, taskId, text }, opt)
|
||||||
taskId,
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.add', { taskId, text })
|
||||||
text
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
})
|
else {
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
bareP2pPrint(ctx, 'added ' + taskId, opt)
|
||||||
else ctx.console.log('added ' + taskId)
|
bareP2pMaybeNext(ctx, opt, argv0 + ' list')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'done') {
|
if (sub === 'done') {
|
||||||
const taskId = String(args[1] || '').trim()
|
const taskId = String(args[1] || '').trim()
|
||||||
if (!taskId) {
|
if (!taskId) return bareP2pError(ctx, argv0, 'done requires TASK_ID', argv0 + ' list', opt)
|
||||||
ctx.console.error(argv0 + ': done requires TASK_ID')
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes to mark done', opt)
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const r = bareP2pSend(ctx, 'taskmesh', 'task.done', { taskId })
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.done', { taskId })
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
else ctx.console.log('completed ' + taskId)
|
else bareP2pPrint(ctx, 'completed ' + taskId, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const mode = args[1] === 'all' ? 'all' : 'open'
|
const mode = args[1] === 'all' ? 'all' : 'open'
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'taskmesh', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'taskmesh', 2000)
|
||||||
/** @type {Map<string, { taskId: string, text: string, done: boolean, atMs: number }>} */
|
|
||||||
const board = new Map()
|
const board = new Map()
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const kind = row.packet?.kind
|
const kind = row.packet?.kind
|
||||||
const p = row.packet?.payload
|
const p = row.packet?.payload
|
||||||
if (!p || typeof p !== 'object') continue
|
if (!p || typeof p !== 'object') continue
|
||||||
if (kind === 'task.add' && typeof p.taskId === 'string') {
|
if (kind === 'task.add' && typeof p.taskId === 'string') {
|
||||||
board.set(p.taskId, {
|
board.set(p.taskId, { taskId: p.taskId, text: typeof p.text === 'string' ? p.text : '', done: false, atMs: row.receivedAtMs })
|
||||||
taskId: p.taskId,
|
|
||||||
text: typeof p.text === 'string' ? p.text : '',
|
|
||||||
done: false,
|
|
||||||
atMs: row.receivedAtMs
|
|
||||||
})
|
|
||||||
} else if (kind === 'task.done' && typeof p.taskId === 'string') {
|
} else if (kind === 'task.done' && typeof p.taskId === 'string') {
|
||||||
const cur = board.get(p.taskId)
|
const cur = board.get(p.taskId)
|
||||||
if (cur) cur.done = true
|
if (cur) cur.done = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const all = [...board.values()].sort((a, b) => a.atMs - b.atMs)
|
const all = [...board.values()].sort((a, b) => a.atMs - b.atMs)
|
||||||
|
if (opt.summary) return bareP2pPrint(ctx, { tasks: all.length, open: all.filter((x)=>!x.done).length }, opt)
|
||||||
for (const t of all) {
|
for (const t of all) {
|
||||||
if (mode !== 'all' && t.done) continue
|
if (mode !== 'all' && t.done) continue
|
||||||
ctx.console.log((t.done ? '[x] ' : '[ ] ') + t.taskId + ' ' + t.text)
|
bareP2pPrint(ctx, (t.done ? '[x] ' : '[ ] ') + t.taskId + ' ' + t.text, opt)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['add', 'done', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema": 2,
|
"schema": 2,
|
||||||
"profileId": "bare-os-posix-like",
|
"profileId": "bare-os-posix-like",
|
||||||
"generatedAt": "2026-04-26T12:27:28.060Z",
|
"generatedAt": "2026-04-26T12:40:06.854Z",
|
||||||
"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": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"atMs": 1777206448060,
|
"atMs": 1777207206853,
|
||||||
"commands": [
|
"commands": [
|
||||||
"agent",
|
"agent",
|
||||||
"arch",
|
"arch",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -10,6 +10,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'dhtscan'
|
const argv0 = argv[0] || 'dhtscan'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
'\n' +
|
'Print compact DHT scan summary from /proc/bare_os/dht_scan.json.',
|
||||||
'Print compact DHT scan summary from /proc/bare_os/dht_scan.json.\n' +
|
argv0 + ' [--summary] [--json]',
|
||||||
'See man dhtscan.'
|
['--summary', '--json'],
|
||||||
|
['dhttop', 'swarmdoctor']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
||||||
ctx.console.log(JSON.stringify(dht, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: dht.peers ?? null, firewalled: dht?.dhtStatus?.firewalled ?? null } : dht, opt)
|
||||||
|
bareP2pMaybeNext(ctx, opt, 'dhttop')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'dhttop'
|
const argv0 = argv[0] || 'dhttop'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' + argv0 + '\n' + 'Live DHT dashboard TUI.\n' + 'See man dhttop.'
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'Live DHT dashboard TUI.',
|
||||||
|
argv0 + ' [--summary] [--json]',
|
||||||
|
['--summary', '--json'],
|
||||||
|
['dhtscan', 'swarmdoctor']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
||||||
ctx.console.log(JSON.stringify(dht, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: dht.peers ?? null, firewalled: dht?.dhtStatus?.firewalled ?? null } : dht, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
@@ -25,6 +32,7 @@ async function run(ctx, argv) {
|
|||||||
lines.push('firewalled: ' + String(dht.dhtStatus?.firewalled ?? '?'))
|
lines.push('firewalled: ' + String(dht.dhtStatus?.firewalled ?? '?'))
|
||||||
lines.push('randomized: ' + String(dht.dhtStatus?.randomized ?? '?'))
|
lines.push('randomized: ' + String(dht.dhtStatus?.randomized ?? '?'))
|
||||||
lines.push('bootstrap: ' + String(dht.bootstrapHint || '(default)'))
|
lines.push('bootstrap: ' + String(dht.bootstrapHint || '(default)'))
|
||||||
|
lines.push('last update age ms: ' + String(Math.max(0, Date.now() - Number(dht.atMs || Date.now()))))
|
||||||
lines.push('')
|
lines.push('')
|
||||||
lines.push('raw:')
|
lines.push('raw:')
|
||||||
lines.push(JSON.stringify(dht))
|
lines.push(JSON.stringify(dht))
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'holepunch-view'
|
const argv0 = argv[0] || 'holepunch-view'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
'\n' +
|
'NAT and holepunch summary dashboard.',
|
||||||
'NAT and holepunch summary dashboard.\n' +
|
argv0 + ' [--summary] [--json]',
|
||||||
'See man holepunch-view.'
|
['--summary', '--json'],
|
||||||
|
['routeview', 'dhtscan']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
const h = await bareP2pReadProcJson(ctx, '/proc/bare_os/holepunch_summary.json')
|
const h = await bareP2pReadProcJson(ctx, '/proc/bare_os/holepunch_summary.json')
|
||||||
ctx.console.log(JSON.stringify(h, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: h.peers ?? null, firewalled: h.firewalled ?? null } : h, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'hypershell-board'
|
const argv0 = argv[0] || 'hypershell-board'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' offer-shell LABEL\n' +
|
'P2P hypershell-style session board (intent + audit feed).',
|
||||||
' ' +
|
argv0 + ' offer-shell LABEL | offer-copy PATH | claim SESSION_ID | close SESSION_ID | list',
|
||||||
argv0 +
|
['offer-shell "ops shell"', 'claim session-xxx --yes', 'list --summary'],
|
||||||
' offer-copy PATH\n' +
|
['taskmesh', 'peerctl']
|
||||||
' ' +
|
)
|
||||||
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
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -28,53 +20,40 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'offer-shell' || sub === 'offer-copy') {
|
if (sub === 'offer-shell' || sub === 'offer-copy') {
|
||||||
const label = args.slice(1).join(' ').trim()
|
const label = args.slice(1).join(' ').trim()
|
||||||
if (!label) {
|
if (!label) return bareP2pError(ctx, argv0, 'missing label/path', argv0 + ' --help', opt)
|
||||||
ctx.console.error(argv0 + ': missing label/path')
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sessionId = bareP2pId('session')
|
const sessionId = bareP2pId('session')
|
||||||
const mode = sub === 'offer-shell' ? 'shell' : 'copy'
|
const mode = sub === 'offer-shell' ? 'shell' : 'copy'
|
||||||
const r = bareP2pSend(ctx, 'hypershell-board', 'session.offer', {
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, sessionId, mode, label }, opt)
|
||||||
sessionId,
|
const r = bareP2pSend(ctx, 'hypershell-board', 'session.offer', { sessionId, mode, label })
|
||||||
mode,
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
label
|
else bareP2pPrint(ctx, 'offered ' + sessionId + ' (' + mode + ')', opt)
|
||||||
})
|
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
|
||||||
else ctx.console.log('offered ' + sessionId + ' (' + mode + ')')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'claim' || sub === 'close') {
|
if (sub === 'claim' || sub === 'close') {
|
||||||
const sessionId = String(args[1] || '').trim()
|
const sessionId = String(args[1] || '').trim()
|
||||||
if (!sessionId) {
|
if (!sessionId) return bareP2pError(ctx, argv0, 'missing SESSION_ID', argv0 + ' list', opt)
|
||||||
ctx.console.error(argv0 + ': missing SESSION_ID')
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes for ' + sub, opt)
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const kind = sub === 'claim' ? 'session.claim' : 'session.close'
|
const kind = sub === 'claim' ? 'session.claim' : 'session.close'
|
||||||
const r = bareP2pSend(ctx, 'hypershell-board', kind, { sessionId })
|
const r = bareP2pSend(ctx, 'hypershell-board', kind, { sessionId })
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
else ctx.console.log(sub + ' ' + sessionId)
|
else bareP2pPrint(ctx, sub + ' ' + sessionId, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'hypershell-board', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'hypershell-board', 2000)
|
||||||
/** @type {Map<string, { sessionId: string, mode: string, label: string, state: string, atMs: number }>} */
|
|
||||||
const sessions = new Map()
|
const sessions = new Map()
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const kind = row.packet?.kind
|
const kind = row.packet?.kind
|
||||||
const p = row.packet?.payload
|
const p = row.packet?.payload
|
||||||
if (!p || typeof p !== 'object') continue
|
if (!p || typeof p !== 'object') continue
|
||||||
if (kind === 'session.offer' && typeof p.sessionId === 'string') {
|
if (kind === 'session.offer' && typeof p.sessionId === 'string') {
|
||||||
sessions.set(p.sessionId, {
|
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 })
|
||||||
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') {
|
} else if (kind === 'session.claim' && typeof p.sessionId === 'string') {
|
||||||
const s = sessions.get(p.sessionId)
|
const s = sessions.get(p.sessionId)
|
||||||
if (s) s.state = 'claimed'
|
if (s) s.state = 'claimed'
|
||||||
@@ -83,21 +62,12 @@ async function run(ctx, argv) {
|
|||||||
if (s) s.state = 'closed'
|
if (s) s.state = 'closed'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const s of [...sessions.values()].sort((a, b) => b.atMs - a.atMs)) {
|
const arr=[...sessions.values()].sort((a,b)=>b.atMs-a.atMs)
|
||||||
ctx.console.log(
|
if (opt.summary) return bareP2pPrint(ctx, { sessions: arr.length, open: arr.filter((x)=>x.state==='open').length }, opt)
|
||||||
'[' +
|
for (const s of arr) bareP2pPrint(ctx, '[' + s.state + '] ' + s.sessionId + ' ' + s.mode + ' ' + s.label, opt)
|
||||||
s.state +
|
|
||||||
'] ' +
|
|
||||||
s.sessionId +
|
|
||||||
' ' +
|
|
||||||
s.mode +
|
|
||||||
' ' +
|
|
||||||
s.label
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['offer-shell', 'offer-copy', 'claim', 'close', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,32 +49,19 @@ function bareMeshdropEnsureTransfer(db, transferId) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'meshdrop'
|
const argv0 = argv[0] || 'meshdrop'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'P2P file inbox/outbox over bare-p2p envelopes on chat transport.',
|
||||||
argv0 +
|
argv0 +
|
||||||
' offer FILE [--to PEER_HINT]\n' +
|
' offer FILE [--to PEER_HINT] | send-next OFFER_ID [MAX_CHUNKS] | inbox [N] | accept OFFER_ID | fetch OFFER_ID [DEST] | status [TRANSFER_ID] | cancel TRANSFER_ID',
|
||||||
' ' +
|
['offer ./file.bin --to peerHint', 'send-next offer-123 8', 'status --summary'],
|
||||||
argv0 +
|
['swarmtop', 'peerctl']
|
||||||
' 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
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -84,16 +71,16 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'offer') {
|
if (sub === 'offer') {
|
||||||
const file = args[1]
|
const file = args[1]
|
||||||
if (!file) {
|
if (!file) {
|
||||||
ctx.console.error(argv0 + ': offer requires FILE')
|
return bareP2pError(ctx, argv0, 'offer requires FILE', argv0 + ' --help', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const b = await ctx.vfs.readFile(file)
|
const b = await ctx.vfs.readFile(file)
|
||||||
if (!b) {
|
if (!b) {
|
||||||
ctx.console.error(argv0 + ': unable to read ' + file)
|
return bareP2pError(ctx, argv0, 'unable to read ' + file, 'check file path', opt)
|
||||||
ctx.exitCode = 1
|
if (opt.dryRun) {
|
||||||
|
bareP2pPrint(ctx, { ok: true, dryRun: true, action: 'offer', file, to }, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const b64 = ctx.b4a.toString(b, 'base64')
|
const b64 = ctx.b4a.toString(b, 'base64')
|
||||||
const toIdx = args.indexOf('--to')
|
const toIdx = args.indexOf('--to')
|
||||||
const to = toIdx >= 0 ? String(args[toIdx + 1] || '').trim() : ''
|
const to = toIdx >= 0 ? String(args[toIdx + 1] || '').trim() : ''
|
||||||
@@ -150,22 +137,17 @@ async function run(ctx, argv) {
|
|||||||
to
|
to
|
||||||
})
|
})
|
||||||
if (r && r.ok === false) {
|
if (r && r.ok === false) {
|
||||||
ctx.console.error(argv0 + ': ' + String(r.reason || 'send failed'))
|
return bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, 'offered ' + file + ' as ' + offerId + ' (' + chunksTotal + ' chunks)', opt)
|
||||||
'offered ' + file + ' as ' + offerId + ' (' + chunksTotal + ' chunks)'
|
bareP2pMaybeNext(ctx, opt, argv0 + ' send-next ' + offerId)
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'send-next') {
|
if (sub === 'send-next') {
|
||||||
const offerId = String(args[1] || '').trim()
|
const offerId = String(args[1] || '').trim()
|
||||||
if (!offerId) {
|
if (!offerId) {
|
||||||
ctx.console.error(argv0 + ': send-next requires OFFER_ID')
|
return bareP2pError(ctx, argv0, 'send-next requires OFFER_ID', argv0 + ' status', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const maxChunks = Math.max(
|
const maxChunks = Math.max(
|
||||||
1,
|
1,
|
||||||
@@ -174,9 +156,7 @@ async function run(ctx, argv) {
|
|||||||
const db = await bareMeshdropDbRead(ctx)
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
const rec = db?.outgoing?.[offerId]
|
const rec = db?.outgoing?.[offerId]
|
||||||
if (!rec) {
|
if (!rec) {
|
||||||
ctx.console.error(argv0 + ': unknown offer: ' + offerId)
|
return bareP2pError(ctx, argv0, 'unknown offer: ' + offerId, argv0 + ' status', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let sent = 0
|
let sent = 0
|
||||||
while (sent < maxChunks && rec.sentChunks < rec.chunksTotal) {
|
while (sent < maxChunks && rec.sentChunks < rec.chunksTotal) {
|
||||||
@@ -205,9 +185,7 @@ async function run(ctx, argv) {
|
|||||||
sentChunks: rec.sentChunks,
|
sentChunks: rec.sentChunks,
|
||||||
chunksTotal: rec.chunksTotal
|
chunksTotal: rec.chunksTotal
|
||||||
})
|
})
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, 'sent ' + sent + ' chunk(s), ' + rec.sentChunks + '/' + rec.chunksTotal, opt)
|
||||||
'sent ' + sent + ' chunk(s), ' + rec.sentChunks + '/' + rec.chunksTotal
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +200,7 @@ async function run(ctx, argv) {
|
|||||||
const bytes = typeof p.byteLength === 'number' ? p.byteLength : 0
|
const bytes = typeof p.byteLength === 'number' ? p.byteLength : 0
|
||||||
const chunks = typeof p.chunksTotal === 'number' ? p.chunksTotal : '?'
|
const chunks = typeof p.chunksTotal === 'number' ? p.chunksTotal : '?'
|
||||||
const from = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
const from = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx,
|
||||||
'[' +
|
'[' +
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
'] ' +
|
'] ' +
|
||||||
@@ -235,7 +213,7 @@ async function run(ctx, argv) {
|
|||||||
String(chunks) +
|
String(chunks) +
|
||||||
' from=' +
|
' from=' +
|
||||||
from
|
from
|
||||||
)
|
, opt)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -243,9 +221,7 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'accept') {
|
if (sub === 'accept') {
|
||||||
const offerId = String(args[1] || '').trim()
|
const offerId = String(args[1] || '').trim()
|
||||||
if (!offerId) {
|
if (!offerId) {
|
||||||
ctx.console.error(argv0 + ': accept requires OFFER_ID')
|
return bareP2pError(ctx, argv0, 'accept requires OFFER_ID', argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
const offer = rows.findLast(
|
const offer = rows.findLast(
|
||||||
@@ -256,9 +232,7 @@ async function run(ctx, argv) {
|
|||||||
r.packet.payload.offerId === offerId
|
r.packet.payload.offerId === offerId
|
||||||
)
|
)
|
||||||
if (!offer) {
|
if (!offer) {
|
||||||
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
return bareP2pError(ctx, argv0, 'offer not found: ' + offerId, argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const p = offer.packet.payload
|
const p = offer.packet.payload
|
||||||
const transferId =
|
const transferId =
|
||||||
@@ -289,16 +263,14 @@ async function run(ctx, argv) {
|
|||||||
tr.updatedAtMs = Date.now()
|
tr.updatedAtMs = Date.now()
|
||||||
await bareMeshdropDbWrite(ctx, db)
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
bareP2pSend(ctx, 'meshdrop', 'accept', { offerId, transferId })
|
bareP2pSend(ctx, 'meshdrop', 'accept', { offerId, transferId })
|
||||||
ctx.console.log('accepted ' + offerId + ' (transfer ' + transferId + ')')
|
bareP2pPrint(ctx, 'accepted ' + offerId + ' (transfer ' + transferId + ')', opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'fetch') {
|
if (sub === 'fetch') {
|
||||||
const offerId = String(args[1] || '').trim()
|
const offerId = String(args[1] || '').trim()
|
||||||
if (!offerId) {
|
if (!offerId) {
|
||||||
ctx.console.error(argv0 + ': fetch requires OFFER_ID')
|
return bareP2pError(ctx, argv0, 'fetch requires OFFER_ID', argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
const offer = rows.findLast(
|
const offer = rows.findLast(
|
||||||
@@ -309,9 +281,7 @@ async function run(ctx, argv) {
|
|||||||
r.packet.payload.offerId === offerId
|
r.packet.payload.offerId === offerId
|
||||||
)
|
)
|
||||||
if (!offer) {
|
if (!offer) {
|
||||||
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
return bareP2pError(ctx, argv0, 'offer not found: ' + offerId, argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const p = offer.packet.payload
|
const p = offer.packet.payload
|
||||||
const fileName =
|
const fileName =
|
||||||
@@ -320,16 +290,7 @@ async function run(ctx, argv) {
|
|||||||
const db = await bareMeshdropDbRead(ctx)
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
const incoming = db?.incoming?.[offerId]
|
const incoming = db?.incoming?.[offerId]
|
||||||
if (!incoming) {
|
if (!incoming) {
|
||||||
ctx.console.error(
|
return bareP2pError(ctx, argv0, 'offer not accepted locally', argv0 + ' accept ' + offerId, opt)
|
||||||
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 chunks = incoming.chunks && typeof incoming.chunks === 'object' ? incoming.chunks : {}
|
||||||
const total = typeof incoming.chunksTotal === 'number' ? incoming.chunksTotal : 0
|
const total = typeof incoming.chunksTotal === 'number' ? incoming.chunksTotal : 0
|
||||||
@@ -337,20 +298,7 @@ async function run(ctx, argv) {
|
|||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
const part = chunks[i]
|
const part = chunks[i]
|
||||||
if (typeof part !== 'string' || !part) {
|
if (typeof part !== 'string' || !part) {
|
||||||
ctx.console.error(
|
return bareP2pError(ctx, argv0, 'missing chunk ' + i + '/' + total, argv0 + ' status ' + incoming.transferId, opt)
|
||||||
argv0 +
|
|
||||||
': missing chunk ' +
|
|
||||||
i +
|
|
||||||
'/' +
|
|
||||||
total +
|
|
||||||
' (run `' +
|
|
||||||
argv0 +
|
|
||||||
' status ' +
|
|
||||||
incoming.transferId +
|
|
||||||
'` to inspect)'
|
|
||||||
)
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
pieces.push(part)
|
pieces.push(part)
|
||||||
}
|
}
|
||||||
@@ -369,7 +317,7 @@ async function run(ctx, argv) {
|
|||||||
transferId: incoming.transferId,
|
transferId: incoming.transferId,
|
||||||
savedTo: dest
|
savedTo: dest
|
||||||
})
|
})
|
||||||
ctx.console.log('saved ' + offerId + ' -> ' + dest)
|
bareP2pPrint(ctx, 'saved ' + offerId + ' -> ' + dest, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,11 +370,23 @@ async function run(ctx, argv) {
|
|||||||
.filter((x) => !transferId || x.transferId === transferId)
|
.filter((x) => !transferId || x.transferId === transferId)
|
||||||
.sort((a, b) => (b.updatedAtMs || 0) - (a.updatedAtMs || 0))
|
.sort((a, b) => (b.updatedAtMs || 0) - (a.updatedAtMs || 0))
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
ctx.console.log('no transfers')
|
bareP2pPrint(ctx, opt.summary ? { transfers: 0 } : 'no transfers', opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (opt.summary) {
|
||||||
|
bareP2pPrint(
|
||||||
|
ctx,
|
||||||
|
{
|
||||||
|
transfers: items.length,
|
||||||
|
active: items.filter((x) => String(x.status || '').includes('send') || String(x.status || '').includes('receiv')).length
|
||||||
|
},
|
||||||
|
opt
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
ctx.console.log(
|
bareP2pPrint(
|
||||||
|
ctx,
|
||||||
(it.transferId || '?') +
|
(it.transferId || '?') +
|
||||||
' role=' +
|
' role=' +
|
||||||
String(it.role || '?') +
|
String(it.role || '?') +
|
||||||
@@ -436,6 +396,8 @@ async function run(ctx, argv) {
|
|||||||
String(it.chunksReceived || 0) +
|
String(it.chunksReceived || 0) +
|
||||||
'/' +
|
'/' +
|
||||||
String(it.chunksTotal || 0)
|
String(it.chunksTotal || 0)
|
||||||
|
,
|
||||||
|
opt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -444,7 +406,10 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'cancel') {
|
if (sub === 'cancel') {
|
||||||
const transferId = String(args[1] || '').trim()
|
const transferId = String(args[1] || '').trim()
|
||||||
if (!transferId) {
|
if (!transferId) {
|
||||||
ctx.console.error(argv0 + ': cancel requires TRANSFER_ID')
|
return bareP2pError(ctx, argv0, 'cancel requires TRANSFER_ID', argv0 + ' status', opt)
|
||||||
|
}
|
||||||
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes to cancel transfer', opt)
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -454,10 +419,10 @@ async function run(ctx, argv) {
|
|||||||
tr.updatedAtMs = Date.now()
|
tr.updatedAtMs = Date.now()
|
||||||
await bareMeshdropDbWrite(ctx, db)
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
bareP2pSend(ctx, 'meshdrop', 'cancel', { transferId })
|
bareP2pSend(ctx, 'meshdrop', 'cancel', { transferId })
|
||||||
ctx.console.log('cancelled ' + transferId)
|
bareP2pPrint(ctx, 'cancelled ' + transferId, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['offer', 'send-next', 'inbox', 'accept', 'fetch', 'status', 'cancel'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'p2ping'
|
const argv0 = argv[0] || 'p2ping'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' [LABEL]\n' +
|
'Emit a p2p ping event and print local swarm timing hints.',
|
||||||
'Emit a p2p ping event and print local swarm timing hints.\n' +
|
argv0 + ' [LABEL]',
|
||||||
'See man p2ping.'
|
['hello', '--summary', '--json'],
|
||||||
|
['swarmtop', 'p2ptrace']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -31,6 +35,11 @@ async function run(ctx, argv) {
|
|||||||
localElapsedMs: t1 - t0,
|
localElapsedMs: t1 - t0,
|
||||||
swarmPeers: swarm.peerCount
|
swarmPeers: swarm.peerCount
|
||||||
}
|
}
|
||||||
ctx.console.log(JSON.stringify(out, null, 2))
|
if (opt.summary) {
|
||||||
if (send && send.ok === false) ctx.exitCode = 1
|
bareP2pPrint(ctx, { ok: out.ok, ms: out.localElapsedMs, peers: out.swarmPeers }, opt)
|
||||||
|
} else {
|
||||||
|
bareP2pPrint(ctx, out, opt)
|
||||||
|
}
|
||||||
|
if (send && send.ok === false) bareP2pError(ctx, argv0, String(out.sendReason || 'send failed'), 'swarmdoctor', opt)
|
||||||
|
else bareP2pMaybeNext(ctx, opt, 'p2ptrace p2ping 20')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'p2ptrace'
|
const argv0 = argv[0] || 'p2ptrace'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' [APP] [N]\n' +
|
'Dump recent p2p events as JSON lines.',
|
||||||
'Dump recent p2p events as JSON lines.\n' +
|
argv0 + ' [APP] [N]',
|
||||||
'APP defaults to all known p2p apps.\n' +
|
['swarmtop 20', 'meshdrop 50', '--summary'],
|
||||||
'See man p2ptrace.'
|
['swarmtop', 'peerctl']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -16,30 +19,21 @@ async function run(ctx, argv) {
|
|||||||
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '200', 10) || 200))
|
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '200', 10) || 200))
|
||||||
const apps = app
|
const apps = app
|
||||||
? [app]
|
? [app]
|
||||||
: [
|
: ['swarmtop','meshdrop','taskmesh','peernote','hypershell-board','p2ping','peerdiscover','peerctl']
|
||||||
'swarmtop',
|
|
||||||
'meshdrop',
|
|
||||||
'taskmesh',
|
|
||||||
'peernote',
|
|
||||||
'hypershell-board',
|
|
||||||
'p2ping',
|
|
||||||
'peerdiscover',
|
|
||||||
'peerctl'
|
|
||||||
]
|
|
||||||
const rows = []
|
const rows = []
|
||||||
for (const a of apps) {
|
for (const a of apps) rows.push(...bareP2pCollectFromHistory(ctx, a, n))
|
||||||
rows.push(...bareP2pCollectFromHistory(ctx, a, n))
|
|
||||||
}
|
|
||||||
rows.sort((a, b) => (a.receivedAtMs || 0) - (b.receivedAtMs || 0))
|
rows.sort((a, b) => (a.receivedAtMs || 0) - (b.receivedAtMs || 0))
|
||||||
|
if (opt.summary) {
|
||||||
|
bareP2pPrint(ctx, { apps: apps.length, events: rows.length, lastAgeMs: rows.length ? Math.max(0, Date.now()-Number(rows[rows.length-1].receivedAtMs||Date.now())) : null }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
for (const r of rows.slice(-n)) {
|
for (const r of rows.slice(-n)) {
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, {
|
||||||
JSON.stringify({
|
|
||||||
atMs: r.receivedAtMs || 0,
|
atMs: r.receivedAtMs || 0,
|
||||||
app: r.packet?.app || '?',
|
app: r.packet?.app || '?',
|
||||||
kind: r.packet?.kind || '?',
|
kind: r.packet?.kind || '?',
|
||||||
from: r.displayName || r.fromPeerKey || '',
|
from: r.displayName || r.fromPeerKey || '',
|
||||||
payload: r.packet?.payload || {}
|
payload: r.packet?.payload || {}
|
||||||
})
|
}, { ...opt, json: true })
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'peerctl'
|
const argv0 = argv[0] || 'peerctl'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' health\n' +
|
'P2P control plane helper over event envelopes.',
|
||||||
' ' +
|
argv0 + ' health | request ACTION [JSON_PAYLOAD] | history [N]',
|
||||||
argv0 +
|
['health --summary', 'request restart {\"service\":\"swarm\"} --dry-run', 'history 20'],
|
||||||
' request ACTION [JSON_PAYLOAD]\n' +
|
['swarmdoctor', 'p2ptrace']
|
||||||
' ' +
|
)
|
||||||
argv0 +
|
|
||||||
' history [N]\n' +
|
|
||||||
'P2P control plane helper over event envelopes.\n' +
|
|
||||||
'See man peerctl.'
|
|
||||||
)
|
)
|
||||||
if (args.length === 0) ctx.exitCode = 1
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -22,25 +20,19 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'health') {
|
if (sub === 'health') {
|
||||||
const swarm = await bareP2pReadSwarmSnapshot(ctx)
|
const swarm = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
const doctor = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
const doctor = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
||||||
ctx.console.log(
|
const o = {
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
schema: 1,
|
schema: 1,
|
||||||
swarmPeers: swarm.peerCount,
|
swarmPeers: swarm.peerCount,
|
||||||
health: doctor.health || 'unknown',
|
health: doctor.health || 'unknown',
|
||||||
remediation: doctor.remediation || []
|
remediation: doctor.remediation || []
|
||||||
},
|
}
|
||||||
null,
|
bareP2pPrint(ctx, opt.summary ? { health: o.health, peers: o.swarmPeers } : o, opt)
|
||||||
2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'request') {
|
if (sub === 'request') {
|
||||||
const action = String(args[1] || '').trim()
|
const action = String(args[1] || '').trim()
|
||||||
if (!action) {
|
if (!action) {
|
||||||
ctx.console.error(argv0 + ': request requires ACTION')
|
bareP2pError(ctx, argv0, 'request requires ACTION', argv0 + ' --help', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let payload = {}
|
let payload = {}
|
||||||
@@ -48,34 +40,46 @@ async function run(ctx, argv) {
|
|||||||
try {
|
try {
|
||||||
payload = JSON.parse(args.slice(2).join(' '))
|
payload = JSON.parse(args.slice(2).join(' '))
|
||||||
} catch {
|
} catch {
|
||||||
ctx.console.error(argv0 + ': invalid JSON payload')
|
bareP2pError(ctx, argv0, 'invalid JSON payload', argv0 + ' request ACTION {\"key\":\"value\"}', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (opt.dryRun) {
|
||||||
|
bareP2pPrint(ctx, { ok: true, dryRun: true, action, payload }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes to send control request', opt)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
const r = bareP2pSend(ctx, 'peerctl', 'request', {
|
const r = bareP2pSend(ctx, 'peerctl', 'request', {
|
||||||
requestId: bareP2pId('ctl'),
|
requestId: bareP2pId('ctl'),
|
||||||
action,
|
action,
|
||||||
payload
|
payload
|
||||||
})
|
})
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
|
else bareP2pMaybeNext(ctx, opt, argv0 + ' history 20')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'history') {
|
if (sub === 'history') {
|
||||||
const n = Math.max(1, Math.min(1000, parseInt(args[1] || '50', 10) || 50))
|
const n = Math.max(1, Math.min(1000, parseInt(args[1] || '50', 10) || 50))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'peerctl', 2000).slice(-n)
|
const rows = bareP2pCollectFromHistory(ctx, 'peerctl', 2000).slice(-n)
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
ctx.console.log(
|
bareP2pPrint(
|
||||||
|
ctx,
|
||||||
'[' +
|
'[' +
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
'] ' +
|
'] ' +
|
||||||
String(row.packet?.kind || '?') +
|
String(row.packet?.kind || '?') +
|
||||||
' ' +
|
' ' +
|
||||||
JSON.stringify(row.packet?.payload || {})
|
JSON.stringify(row.packet?.payload || {})
|
||||||
|
,
|
||||||
|
opt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['health', 'request', 'history'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'peerdiscover'
|
const argv0 = argv[0] || 'peerdiscover'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' announce SERVICE [META]\n' +
|
'Peer service announce/list over p2p event history.',
|
||||||
' ' +
|
argv0 + ' announce SERVICE [META] | list [N]',
|
||||||
argv0 +
|
['announce filesync "v1"', 'list 20 --summary'],
|
||||||
' list [N]\n' +
|
['p2ptrace', 'swarmmap']
|
||||||
'Peer service announce/list over p2p event history.\n' +
|
)
|
||||||
'See man peerdiscover.'
|
|
||||||
)
|
)
|
||||||
if (args.length === 0) ctx.exitCode = 1
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -18,37 +19,24 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'announce') {
|
if (sub === 'announce') {
|
||||||
const service = String(args[1] || '').trim()
|
const service = String(args[1] || '').trim()
|
||||||
if (!service) {
|
if (!service) return bareP2pError(ctx, argv0, 'announce requires SERVICE', argv0 + ' --help', opt)
|
||||||
ctx.console.error(argv0 + ': announce requires SERVICE')
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const meta = args.slice(2).join(' ').trim()
|
const meta = args.slice(2).join(' ').trim()
|
||||||
const r = bareP2pSend(ctx, 'peerdiscover', 'service.announce', {
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, service, meta }, opt)
|
||||||
service,
|
const r = bareP2pSend(ctx, 'peerdiscover', 'service.announce', { service, meta, id: bareP2pId('svc') })
|
||||||
meta,
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
id: bareP2pId('svc')
|
else bareP2pMaybeNext(ctx, opt, argv0 + ' list 20')
|
||||||
})
|
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const n = Math.max(1, Math.min(500, parseInt(args[1] || '50', 10) || 50))
|
const n = Math.max(1, Math.min(500, parseInt(args[1] || '50', 10) || 50))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'peerdiscover', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'peerdiscover', 2000).filter((r) => r.packet?.kind === 'service.announce').slice(-n)
|
||||||
.filter((r) => r.packet?.kind === 'service.announce')
|
if (opt.summary) return bareP2pPrint(ctx, { services: rows.length }, opt)
|
||||||
.slice(-n)
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const p = row.packet?.payload || {}
|
const p = row.packet?.payload || {}
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, '[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + String(p.service || 'unknown') + (p.meta ? ' ' + String(p.meta) : ''), opt)
|
||||||
'[' +
|
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
|
||||||
'] ' +
|
|
||||||
String(p.service || 'unknown') +
|
|
||||||
(p.meta ? ' ' + String(p.meta) : '')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['announce', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'peernote'
|
const argv0 = argv[0] || 'peernote'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' add TEXT\n' +
|
'Shared p2p note stream over bare-p2p envelopes.',
|
||||||
' ' +
|
argv0 + ' add TEXT | list [N]',
|
||||||
argv0 +
|
['add "hello cluster"', 'list 20 --summary'],
|
||||||
' list [N]\n' +
|
['taskmesh', 'p2ptrace']
|
||||||
'Shared p2p note stream over bare-p2p envelopes.\n' +
|
)
|
||||||
'See man peernote.'
|
|
||||||
)
|
)
|
||||||
if (args.length === 0) ctx.exitCode = 1
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -18,31 +19,25 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'add') {
|
if (sub === 'add') {
|
||||||
const text = args.slice(1).join(' ').trim()
|
const text = args.slice(1).join(' ').trim()
|
||||||
if (!text) {
|
if (!text) return bareP2pError(ctx, argv0, 'add requires text', argv0 + ' add "text"', opt)
|
||||||
ctx.console.error(argv0 + ': add requires text')
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, text }, opt)
|
||||||
ctx.exitCode = 1
|
const r = bareP2pSend(ctx, 'peernote', 'note.add', { noteId: bareP2pId('note'), text })
|
||||||
return
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
}
|
else bareP2pMaybeNext(ctx, opt, argv0 + ' list 20')
|
||||||
const r = bareP2pSend(ctx, 'peernote', 'note.add', {
|
|
||||||
noteId: bareP2pId('note'),
|
|
||||||
text
|
|
||||||
})
|
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const n = Math.max(1, Math.min(300, parseInt(args[1] || '30', 10) || 30))
|
const n = Math.max(1, Math.min(300, parseInt(args[1] || '30', 10) || 30))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'peernote', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'peernote', 2000).filter((r) => r.packet?.kind === 'note.add').slice(-n)
|
||||||
.filter((r) => r.packet?.kind === 'note.add')
|
if (opt.summary) return bareP2pPrint(ctx, { notes: rows.length }, opt)
|
||||||
.slice(-n)
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const p = row.packet?.payload || {}
|
const p = row.packet?.payload || {}
|
||||||
const text = typeof p.text === 'string' ? p.text : ''
|
const text = typeof p.text === 'string' ? p.text : ''
|
||||||
const who = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
const who = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
ctx.console.log('[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + who + ': ' + text)
|
bareP2pPrint(ctx, '[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + who + ': ' + text, opt)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['add', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'routeview'
|
const argv0 = argv[0] || 'routeview'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' + argv0 + '\n' + 'Route and relay visibility TUI.\n' + 'See man routeview.'
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'Route and relay visibility TUI.',
|
||||||
|
argv0 + ' [--summary] [--json]',
|
||||||
|
['--summary', '--json'],
|
||||||
|
['holepunch-view', 'swarmdoctor']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
const r = await bareP2pReadProcJson(ctx, '/proc/bare_os/route_summary.json')
|
const r = await bareP2pReadProcJson(ctx, '/proc/bare_os/route_summary.json')
|
||||||
ctx.console.log(JSON.stringify(r, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: r.peers ?? null, direct: r.directLikely ?? null, relay: r.relayLikely ?? null } : r, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'swarmdoctor'
|
const argv0 = argv[0] || 'swarmdoctor'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
'\n' +
|
'Print swarm diagnosis from /proc/bare_os/swarm_doctor.json.',
|
||||||
'Print swarm diagnosis from /proc/bare_os/swarm_doctor.json.\n' +
|
argv0 + ' [--summary] [--json]',
|
||||||
'See man swarmdoctor.'
|
['--summary', '--json'],
|
||||||
|
['swarmtop', 'peerctl']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const doc = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
const doc = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
||||||
ctx.console.log(JSON.stringify(doc, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { health: doc.health || 'unknown', findings: (doc.findings && doc.findings.length) || 0 } : doc, opt)
|
||||||
if (doc && typeof doc === 'object' && doc.health && doc.health !== 'ok') {
|
if (doc && typeof doc === 'object' && doc.health && doc.health !== 'ok') {
|
||||||
|
if (!opt.quiet) ctx.console.error('hint: check routeview and holepunch-view for network posture')
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'swarmmap'
|
const argv0 = argv[0] || 'swarmmap'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' + argv0 + '\n' + 'Swarm topology map TUI.\n' + 'See man swarmmap.'
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'Swarm topology map TUI.',
|
||||||
|
argv0 + ' [--summary] [--json]',
|
||||||
|
['--summary', '--json'],
|
||||||
|
['swarmtop', 'peerdiscover']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
ctx.console.log(JSON.stringify(snap, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: snap.peerCount, topics: snap.topicCount } : snap, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
|
|||||||
@@ -1,35 +1,45 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'swarmtop'
|
const argv0 = argv[0] || 'swarmtop'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' [watch | events [N] | ping [LABEL]]\n' +
|
'P2P fleet dashboard over Bare OS chat + /proc swarm data.',
|
||||||
'P2P fleet dashboard over Bare OS chat + /proc swarm data.\n' +
|
argv0 + ' [watch | events [N] | ping [LABEL]]',
|
||||||
'No args on a TTY opens full-screen swarmtop.\n' +
|
['ping hello', 'events 20', 'watch --json'],
|
||||||
'See man swarmtop.'
|
['swarmdoctor', 'swarmmap', 'p2ptrace']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
bareP2pFirstRunHint(ctx, 'swarmtop', "run 'swarmdoctor' when peer rows look incomplete")
|
||||||
|
|
||||||
const sub = args[0] || ''
|
const sub = args[0] || ''
|
||||||
if (sub === 'ping') {
|
if (sub === 'ping') {
|
||||||
const label = args.slice(1).join(' ').trim() || 'hello'
|
const label = args.slice(1).join(' ').trim() || 'hello'
|
||||||
|
if (opt.dryRun) {
|
||||||
|
bareP2pPrint(ctx, { ok: true, dryRun: true, action: 'ping', label }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
const r = bareP2pSend(ctx, 'swarmtop', 'ping', {
|
const r = bareP2pSend(ctx, 'swarmtop', 'ping', {
|
||||||
id: bareP2pId('ping'),
|
id: bareP2pId('ping'),
|
||||||
label
|
label
|
||||||
})
|
})
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
|
else bareP2pMaybeNext(ctx, opt, 'swarmtop events 20')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'events') {
|
if (sub === 'events') {
|
||||||
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '30', 10) || 30))
|
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '30', 10) || 30))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'swarmtop', n)
|
const rows = bareP2pCollectFromHistory(ctx, 'swarmtop', n)
|
||||||
|
const out = []
|
||||||
for (const row of rows.slice(-n)) {
|
for (const row of rows.slice(-n)) {
|
||||||
const pkt = row.packet || {}
|
const pkt = row.packet || {}
|
||||||
const payload = pkt.payload && typeof pkt.payload === 'object' ? pkt.payload : {}
|
const payload = pkt.payload && typeof pkt.payload === 'object' ? pkt.payload : {}
|
||||||
ctx.console.log(
|
out.push(
|
||||||
'[' +
|
'[' +
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
'] ' +
|
'] ' +
|
||||||
@@ -38,6 +48,12 @@ async function run(ctx, argv) {
|
|||||||
JSON.stringify(payload)
|
JSON.stringify(payload)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (!out.length) out.push('(none)')
|
||||||
|
if (opt.summary) {
|
||||||
|
bareP2pPrint(ctx, { app: 'swarmtop', events: out.length }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const ln of out) bareP2pPrint(ctx, ln, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +101,7 @@ async function run(ctx, argv) {
|
|||||||
lines.push('Peers:')
|
lines.push('Peers:')
|
||||||
const peers = Array.isArray(snap.peers) ? snap.peers : []
|
const peers = Array.isArray(snap.peers) ? snap.peers : []
|
||||||
if (!peers.length) lines.push(' (no peers reported)')
|
if (!peers.length) lines.push(' (no peers reported)')
|
||||||
|
let unknownCount = 0
|
||||||
for (const p of peers.slice(0, 12)) {
|
for (const p of peers.slice(0, 12)) {
|
||||||
const key =
|
const key =
|
||||||
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
|
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
|
||||||
@@ -94,12 +111,19 @@ async function run(ctx, argv) {
|
|||||||
p && typeof p === 'object' && typeof p.state === 'string'
|
p && typeof p === 'object' && typeof p.state === 'string'
|
||||||
? p.state
|
? p.state
|
||||||
: 'connected'
|
: 'connected'
|
||||||
const addr =
|
const endpointObj =
|
||||||
p && typeof p === 'object' && p.endpoint && typeof p.endpoint === 'object'
|
p && typeof p === 'object' && p.endpoint && typeof p.endpoint === 'object'
|
||||||
? String(p.endpoint.address || p.remoteAddress || '?') +
|
? p.endpoint
|
||||||
':' +
|
: null
|
||||||
String(p.endpoint.port || p.remotePort || '?')
|
const addrRaw = String(
|
||||||
: String(p?.remoteAddress || '?') + ':' + String(p?.remotePort || '?')
|
(endpointObj && endpointObj.address) ||
|
||||||
|
(p && typeof p === 'object' ? p.remoteAddress || '' : '')
|
||||||
|
).trim()
|
||||||
|
const portRaw = String(
|
||||||
|
(endpointObj && endpointObj.port) ||
|
||||||
|
(p && typeof p === 'object' ? p.remotePort || '' : '')
|
||||||
|
).trim()
|
||||||
|
const addr = addrRaw && portRaw ? addrRaw + ':' + portRaw : 'unavailable'
|
||||||
const osHint =
|
const osHint =
|
||||||
p &&
|
p &&
|
||||||
typeof p === 'object' &&
|
typeof p === 'object' &&
|
||||||
@@ -108,12 +132,27 @@ async function run(ctx, argv) {
|
|||||||
p.peerOs.osHint
|
p.peerOs.osHint
|
||||||
? String(p.peerOs.osHint)
|
? String(p.peerOs.osHint)
|
||||||
: 'unknown'
|
: 'unknown'
|
||||||
|
const osConfidence = osHint === 'unknown' ? 'unknown' : 'reported'
|
||||||
|
if (addr === 'unavailable' || osHint === 'unknown') unknownCount++
|
||||||
lines.push(' pubkey: ' + key)
|
lines.push(' pubkey: ' + key)
|
||||||
lines.push(' state=' + state + ' endpoint=' + addr + ' os=' + osHint)
|
lines.push(
|
||||||
|
' state=' +
|
||||||
|
state +
|
||||||
|
' endpoint=' +
|
||||||
|
addr +
|
||||||
|
' os=' +
|
||||||
|
osHint +
|
||||||
|
' osConfidence=' +
|
||||||
|
osConfidence
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
lines.push(' peerData diagnostics: incomplete=' + unknownCount + '/' + peers.length)
|
||||||
lines.push('')
|
lines.push('')
|
||||||
lines.push('Recent events:')
|
lines.push('Recent events:')
|
||||||
if (!events.length) lines.push(' (none)')
|
if (!events.length) {
|
||||||
|
lines.push(' (none)')
|
||||||
|
lines.push(' event ingest: no app events seen yet; try r, then swarmtop ping hello')
|
||||||
|
}
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
lines.push(
|
lines.push(
|
||||||
' [' +
|
' [' +
|
||||||
@@ -130,10 +169,10 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
if (sub === 'watch') {
|
if (sub === 'watch') {
|
||||||
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
ctx.console.log(JSON.stringify(snap, null, 2))
|
bareP2pPrint(ctx, snap, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['watch', 'events', 'ping'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'taskmesh'
|
const argv0 = argv[0] || 'taskmesh'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' add TEXT\n' +
|
'P2P task board over append-only bare-p2p events.',
|
||||||
' ' +
|
argv0 + ' add TEXT | done TASK_ID | list [open|all]',
|
||||||
argv0 +
|
['add "ship release"', 'list all', 'done task-abc'],
|
||||||
' done TASK_ID\n' +
|
['peernote', 'p2ptrace']
|
||||||
' ' +
|
)
|
||||||
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
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -22,63 +20,56 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'add') {
|
if (sub === 'add') {
|
||||||
const text = args.slice(1).join(' ').trim()
|
const text = args.slice(1).join(' ').trim()
|
||||||
if (!text) {
|
if (!text) return bareP2pError(ctx, argv0, 'add requires text', argv0 + ' add "text"', opt)
|
||||||
ctx.console.error(argv0 + ': add requires text')
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const taskId = bareP2pId('task')
|
const taskId = bareP2pId('task')
|
||||||
const r = bareP2pSend(ctx, 'taskmesh', 'task.add', {
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, taskId, text }, opt)
|
||||||
taskId,
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.add', { taskId, text })
|
||||||
text
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
})
|
else {
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
bareP2pPrint(ctx, 'added ' + taskId, opt)
|
||||||
else ctx.console.log('added ' + taskId)
|
bareP2pMaybeNext(ctx, opt, argv0 + ' list')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'done') {
|
if (sub === 'done') {
|
||||||
const taskId = String(args[1] || '').trim()
|
const taskId = String(args[1] || '').trim()
|
||||||
if (!taskId) {
|
if (!taskId) return bareP2pError(ctx, argv0, 'done requires TASK_ID', argv0 + ' list', opt)
|
||||||
ctx.console.error(argv0 + ': done requires TASK_ID')
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes to mark done', opt)
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const r = bareP2pSend(ctx, 'taskmesh', 'task.done', { taskId })
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.done', { taskId })
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
else ctx.console.log('completed ' + taskId)
|
else bareP2pPrint(ctx, 'completed ' + taskId, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const mode = args[1] === 'all' ? 'all' : 'open'
|
const mode = args[1] === 'all' ? 'all' : 'open'
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'taskmesh', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'taskmesh', 2000)
|
||||||
/** @type {Map<string, { taskId: string, text: string, done: boolean, atMs: number }>} */
|
|
||||||
const board = new Map()
|
const board = new Map()
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const kind = row.packet?.kind
|
const kind = row.packet?.kind
|
||||||
const p = row.packet?.payload
|
const p = row.packet?.payload
|
||||||
if (!p || typeof p !== 'object') continue
|
if (!p || typeof p !== 'object') continue
|
||||||
if (kind === 'task.add' && typeof p.taskId === 'string') {
|
if (kind === 'task.add' && typeof p.taskId === 'string') {
|
||||||
board.set(p.taskId, {
|
board.set(p.taskId, { taskId: p.taskId, text: typeof p.text === 'string' ? p.text : '', done: false, atMs: row.receivedAtMs })
|
||||||
taskId: p.taskId,
|
|
||||||
text: typeof p.text === 'string' ? p.text : '',
|
|
||||||
done: false,
|
|
||||||
atMs: row.receivedAtMs
|
|
||||||
})
|
|
||||||
} else if (kind === 'task.done' && typeof p.taskId === 'string') {
|
} else if (kind === 'task.done' && typeof p.taskId === 'string') {
|
||||||
const cur = board.get(p.taskId)
|
const cur = board.get(p.taskId)
|
||||||
if (cur) cur.done = true
|
if (cur) cur.done = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const all = [...board.values()].sort((a, b) => a.atMs - b.atMs)
|
const all = [...board.values()].sort((a, b) => a.atMs - b.atMs)
|
||||||
|
if (opt.summary) return bareP2pPrint(ctx, { tasks: all.length, open: all.filter((x)=>!x.done).length }, opt)
|
||||||
for (const t of all) {
|
for (const t of all) {
|
||||||
if (mode !== 'all' && t.done) continue
|
if (mode !== 'all' && t.done) continue
|
||||||
ctx.console.log((t.done ? '[x] ' : '[ ] ') + t.taskId + ' ' + t.text)
|
bareP2pPrint(ctx, (t.done ? '[x] ' : '[ ] ') + t.taskId + ' ' + t.text, opt)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['add', 'done', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,17 +438,22 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'dhtscan'
|
const argv0 = argv[0] || 'dhtscan'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
'\n' +
|
'Print compact DHT scan summary from /proc/bare_os/dht_scan.json.',
|
||||||
'Print compact DHT scan summary from /proc/bare_os/dht_scan.json.\n' +
|
argv0 + ' [--summary] [--json]',
|
||||||
'See man dhtscan.'
|
['--summary', '--json'],
|
||||||
|
['dhttop', 'swarmdoctor']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
||||||
ctx.console.log(JSON.stringify(dht, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: dht.peers ?? null, firewalled: dht?.dhtStatus?.firewalled ?? null } : dht, opt)
|
||||||
|
bareP2pMaybeNext(ctx, opt, 'dhttop')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -570,6 +570,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -920,18 +1028,25 @@ async function bareP2pRunSimpleTui(ctx, opts) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'dhttop'
|
const argv0 = argv[0] || 'dhttop'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' + argv0 + '\n' + 'Live DHT dashboard TUI.\n' + 'See man dhttop.'
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'Live DHT dashboard TUI.',
|
||||||
|
argv0 + ' [--summary] [--json]',
|
||||||
|
['--summary', '--json'],
|
||||||
|
['dhtscan', 'swarmdoctor']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
const dht = await bareP2pReadProcJson(ctx, '/proc/bare_os/dht_scan.json')
|
||||||
ctx.console.log(JSON.stringify(dht, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: dht.peers ?? null, firewalled: dht?.dhtStatus?.firewalled ?? null } : dht, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
@@ -945,6 +1060,7 @@ async function run(ctx, argv) {
|
|||||||
lines.push('firewalled: ' + String(dht.dhtStatus?.firewalled ?? '?'))
|
lines.push('firewalled: ' + String(dht.dhtStatus?.firewalled ?? '?'))
|
||||||
lines.push('randomized: ' + String(dht.dhtStatus?.randomized ?? '?'))
|
lines.push('randomized: ' + String(dht.dhtStatus?.randomized ?? '?'))
|
||||||
lines.push('bootstrap: ' + String(dht.bootstrapHint || '(default)'))
|
lines.push('bootstrap: ' + String(dht.bootstrapHint || '(default)'))
|
||||||
|
lines.push('last update age ms: ' + String(Math.max(0, Date.now() - Number(dht.atMs || Date.now()))))
|
||||||
lines.push('')
|
lines.push('')
|
||||||
lines.push('raw:')
|
lines.push('raw:')
|
||||||
lines.push(JSON.stringify(dht))
|
lines.push(JSON.stringify(dht))
|
||||||
|
|||||||
@@ -570,6 +570,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -920,22 +1028,25 @@ async function bareP2pRunSimpleTui(ctx, opts) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'holepunch-view'
|
const argv0 = argv[0] || 'holepunch-view'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
'\n' +
|
'NAT and holepunch summary dashboard.',
|
||||||
'NAT and holepunch summary dashboard.\n' +
|
argv0 + ' [--summary] [--json]',
|
||||||
'See man holepunch-view.'
|
['--summary', '--json'],
|
||||||
|
['routeview', 'dhtscan']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
const h = await bareP2pReadProcJson(ctx, '/proc/bare_os/holepunch_summary.json')
|
const h = await bareP2pReadProcJson(ctx, '/proc/bare_os/holepunch_summary.json')
|
||||||
ctx.console.log(JSON.stringify(h, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: h.peers ?? null, firewalled: h.firewalled ?? null } : h, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
|
|||||||
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,26 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'hypershell-board'
|
const argv0 = argv[0] || 'hypershell-board'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' offer-shell LABEL\n' +
|
'P2P hypershell-style session board (intent + audit feed).',
|
||||||
' ' +
|
argv0 + ' offer-shell LABEL | offer-copy PATH | claim SESSION_ID | close SESSION_ID | list',
|
||||||
argv0 +
|
['offer-shell "ops shell"', 'claim session-xxx --yes', 'list --summary'],
|
||||||
' offer-copy PATH\n' +
|
['taskmesh', 'peerctl']
|
||||||
' ' +
|
)
|
||||||
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
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -358,53 +458,40 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'offer-shell' || sub === 'offer-copy') {
|
if (sub === 'offer-shell' || sub === 'offer-copy') {
|
||||||
const label = args.slice(1).join(' ').trim()
|
const label = args.slice(1).join(' ').trim()
|
||||||
if (!label) {
|
if (!label) return bareP2pError(ctx, argv0, 'missing label/path', argv0 + ' --help', opt)
|
||||||
ctx.console.error(argv0 + ': missing label/path')
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const sessionId = bareP2pId('session')
|
const sessionId = bareP2pId('session')
|
||||||
const mode = sub === 'offer-shell' ? 'shell' : 'copy'
|
const mode = sub === 'offer-shell' ? 'shell' : 'copy'
|
||||||
const r = bareP2pSend(ctx, 'hypershell-board', 'session.offer', {
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, sessionId, mode, label }, opt)
|
||||||
sessionId,
|
const r = bareP2pSend(ctx, 'hypershell-board', 'session.offer', { sessionId, mode, label })
|
||||||
mode,
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
label
|
else bareP2pPrint(ctx, 'offered ' + sessionId + ' (' + mode + ')', opt)
|
||||||
})
|
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
|
||||||
else ctx.console.log('offered ' + sessionId + ' (' + mode + ')')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'claim' || sub === 'close') {
|
if (sub === 'claim' || sub === 'close') {
|
||||||
const sessionId = String(args[1] || '').trim()
|
const sessionId = String(args[1] || '').trim()
|
||||||
if (!sessionId) {
|
if (!sessionId) return bareP2pError(ctx, argv0, 'missing SESSION_ID', argv0 + ' list', opt)
|
||||||
ctx.console.error(argv0 + ': missing SESSION_ID')
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes for ' + sub, opt)
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const kind = sub === 'claim' ? 'session.claim' : 'session.close'
|
const kind = sub === 'claim' ? 'session.claim' : 'session.close'
|
||||||
const r = bareP2pSend(ctx, 'hypershell-board', kind, { sessionId })
|
const r = bareP2pSend(ctx, 'hypershell-board', kind, { sessionId })
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
else ctx.console.log(sub + ' ' + sessionId)
|
else bareP2pPrint(ctx, sub + ' ' + sessionId, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'hypershell-board', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'hypershell-board', 2000)
|
||||||
/** @type {Map<string, { sessionId: string, mode: string, label: string, state: string, atMs: number }>} */
|
|
||||||
const sessions = new Map()
|
const sessions = new Map()
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const kind = row.packet?.kind
|
const kind = row.packet?.kind
|
||||||
const p = row.packet?.payload
|
const p = row.packet?.payload
|
||||||
if (!p || typeof p !== 'object') continue
|
if (!p || typeof p !== 'object') continue
|
||||||
if (kind === 'session.offer' && typeof p.sessionId === 'string') {
|
if (kind === 'session.offer' && typeof p.sessionId === 'string') {
|
||||||
sessions.set(p.sessionId, {
|
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 })
|
||||||
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') {
|
} else if (kind === 'session.claim' && typeof p.sessionId === 'string') {
|
||||||
const s = sessions.get(p.sessionId)
|
const s = sessions.get(p.sessionId)
|
||||||
if (s) s.state = 'claimed'
|
if (s) s.state = 'claimed'
|
||||||
@@ -413,21 +500,12 @@ async function run(ctx, argv) {
|
|||||||
if (s) s.state = 'closed'
|
if (s) s.state = 'closed'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const s of [...sessions.values()].sort((a, b) => b.atMs - a.atMs)) {
|
const arr=[...sessions.values()].sort((a,b)=>b.atMs-a.atMs)
|
||||||
ctx.console.log(
|
if (opt.summary) return bareP2pPrint(ctx, { sessions: arr.length, open: arr.filter((x)=>x.state==='open').length }, opt)
|
||||||
'[' +
|
for (const s of arr) bareP2pPrint(ctx, '[' + s.state + '] ' + s.sessionId + ' ' + s.mode + ' ' + s.label, opt)
|
||||||
s.state +
|
|
||||||
'] ' +
|
|
||||||
s.sessionId +
|
|
||||||
' ' +
|
|
||||||
s.mode +
|
|
||||||
' ' +
|
|
||||||
s.label
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['offer-shell', 'offer-copy', 'claim', 'close', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -379,32 +487,19 @@ function bareMeshdropEnsureTransfer(db, transferId) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'meshdrop'
|
const argv0 = argv[0] || 'meshdrop'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'P2P file inbox/outbox over bare-p2p envelopes on chat transport.',
|
||||||
argv0 +
|
argv0 +
|
||||||
' offer FILE [--to PEER_HINT]\n' +
|
' offer FILE [--to PEER_HINT] | send-next OFFER_ID [MAX_CHUNKS] | inbox [N] | accept OFFER_ID | fetch OFFER_ID [DEST] | status [TRANSFER_ID] | cancel TRANSFER_ID',
|
||||||
' ' +
|
['offer ./file.bin --to peerHint', 'send-next offer-123 8', 'status --summary'],
|
||||||
argv0 +
|
['swarmtop', 'peerctl']
|
||||||
' 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
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -414,16 +509,16 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'offer') {
|
if (sub === 'offer') {
|
||||||
const file = args[1]
|
const file = args[1]
|
||||||
if (!file) {
|
if (!file) {
|
||||||
ctx.console.error(argv0 + ': offer requires FILE')
|
return bareP2pError(ctx, argv0, 'offer requires FILE', argv0 + ' --help', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const b = await ctx.vfs.readFile(file)
|
const b = await ctx.vfs.readFile(file)
|
||||||
if (!b) {
|
if (!b) {
|
||||||
ctx.console.error(argv0 + ': unable to read ' + file)
|
return bareP2pError(ctx, argv0, 'unable to read ' + file, 'check file path', opt)
|
||||||
ctx.exitCode = 1
|
if (opt.dryRun) {
|
||||||
|
bareP2pPrint(ctx, { ok: true, dryRun: true, action: 'offer', file, to }, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const b64 = ctx.b4a.toString(b, 'base64')
|
const b64 = ctx.b4a.toString(b, 'base64')
|
||||||
const toIdx = args.indexOf('--to')
|
const toIdx = args.indexOf('--to')
|
||||||
const to = toIdx >= 0 ? String(args[toIdx + 1] || '').trim() : ''
|
const to = toIdx >= 0 ? String(args[toIdx + 1] || '').trim() : ''
|
||||||
@@ -480,22 +575,17 @@ async function run(ctx, argv) {
|
|||||||
to
|
to
|
||||||
})
|
})
|
||||||
if (r && r.ok === false) {
|
if (r && r.ok === false) {
|
||||||
ctx.console.error(argv0 + ': ' + String(r.reason || 'send failed'))
|
return bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, 'offered ' + file + ' as ' + offerId + ' (' + chunksTotal + ' chunks)', opt)
|
||||||
'offered ' + file + ' as ' + offerId + ' (' + chunksTotal + ' chunks)'
|
bareP2pMaybeNext(ctx, opt, argv0 + ' send-next ' + offerId)
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'send-next') {
|
if (sub === 'send-next') {
|
||||||
const offerId = String(args[1] || '').trim()
|
const offerId = String(args[1] || '').trim()
|
||||||
if (!offerId) {
|
if (!offerId) {
|
||||||
ctx.console.error(argv0 + ': send-next requires OFFER_ID')
|
return bareP2pError(ctx, argv0, 'send-next requires OFFER_ID', argv0 + ' status', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const maxChunks = Math.max(
|
const maxChunks = Math.max(
|
||||||
1,
|
1,
|
||||||
@@ -504,9 +594,7 @@ async function run(ctx, argv) {
|
|||||||
const db = await bareMeshdropDbRead(ctx)
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
const rec = db?.outgoing?.[offerId]
|
const rec = db?.outgoing?.[offerId]
|
||||||
if (!rec) {
|
if (!rec) {
|
||||||
ctx.console.error(argv0 + ': unknown offer: ' + offerId)
|
return bareP2pError(ctx, argv0, 'unknown offer: ' + offerId, argv0 + ' status', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
let sent = 0
|
let sent = 0
|
||||||
while (sent < maxChunks && rec.sentChunks < rec.chunksTotal) {
|
while (sent < maxChunks && rec.sentChunks < rec.chunksTotal) {
|
||||||
@@ -535,9 +623,7 @@ async function run(ctx, argv) {
|
|||||||
sentChunks: rec.sentChunks,
|
sentChunks: rec.sentChunks,
|
||||||
chunksTotal: rec.chunksTotal
|
chunksTotal: rec.chunksTotal
|
||||||
})
|
})
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, 'sent ' + sent + ' chunk(s), ' + rec.sentChunks + '/' + rec.chunksTotal, opt)
|
||||||
'sent ' + sent + ' chunk(s), ' + rec.sentChunks + '/' + rec.chunksTotal
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,7 +638,7 @@ async function run(ctx, argv) {
|
|||||||
const bytes = typeof p.byteLength === 'number' ? p.byteLength : 0
|
const bytes = typeof p.byteLength === 'number' ? p.byteLength : 0
|
||||||
const chunks = typeof p.chunksTotal === 'number' ? p.chunksTotal : '?'
|
const chunks = typeof p.chunksTotal === 'number' ? p.chunksTotal : '?'
|
||||||
const from = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
const from = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx,
|
||||||
'[' +
|
'[' +
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
'] ' +
|
'] ' +
|
||||||
@@ -565,7 +651,7 @@ async function run(ctx, argv) {
|
|||||||
String(chunks) +
|
String(chunks) +
|
||||||
' from=' +
|
' from=' +
|
||||||
from
|
from
|
||||||
)
|
, opt)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -573,9 +659,7 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'accept') {
|
if (sub === 'accept') {
|
||||||
const offerId = String(args[1] || '').trim()
|
const offerId = String(args[1] || '').trim()
|
||||||
if (!offerId) {
|
if (!offerId) {
|
||||||
ctx.console.error(argv0 + ': accept requires OFFER_ID')
|
return bareP2pError(ctx, argv0, 'accept requires OFFER_ID', argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
const offer = rows.findLast(
|
const offer = rows.findLast(
|
||||||
@@ -586,9 +670,7 @@ async function run(ctx, argv) {
|
|||||||
r.packet.payload.offerId === offerId
|
r.packet.payload.offerId === offerId
|
||||||
)
|
)
|
||||||
if (!offer) {
|
if (!offer) {
|
||||||
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
return bareP2pError(ctx, argv0, 'offer not found: ' + offerId, argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const p = offer.packet.payload
|
const p = offer.packet.payload
|
||||||
const transferId =
|
const transferId =
|
||||||
@@ -619,16 +701,14 @@ async function run(ctx, argv) {
|
|||||||
tr.updatedAtMs = Date.now()
|
tr.updatedAtMs = Date.now()
|
||||||
await bareMeshdropDbWrite(ctx, db)
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
bareP2pSend(ctx, 'meshdrop', 'accept', { offerId, transferId })
|
bareP2pSend(ctx, 'meshdrop', 'accept', { offerId, transferId })
|
||||||
ctx.console.log('accepted ' + offerId + ' (transfer ' + transferId + ')')
|
bareP2pPrint(ctx, 'accepted ' + offerId + ' (transfer ' + transferId + ')', opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'fetch') {
|
if (sub === 'fetch') {
|
||||||
const offerId = String(args[1] || '').trim()
|
const offerId = String(args[1] || '').trim()
|
||||||
if (!offerId) {
|
if (!offerId) {
|
||||||
ctx.console.error(argv0 + ': fetch requires OFFER_ID')
|
return bareP2pError(ctx, argv0, 'fetch requires OFFER_ID', argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'meshdrop', 2000)
|
||||||
const offer = rows.findLast(
|
const offer = rows.findLast(
|
||||||
@@ -639,9 +719,7 @@ async function run(ctx, argv) {
|
|||||||
r.packet.payload.offerId === offerId
|
r.packet.payload.offerId === offerId
|
||||||
)
|
)
|
||||||
if (!offer) {
|
if (!offer) {
|
||||||
ctx.console.error(argv0 + ': offer not found: ' + offerId)
|
return bareP2pError(ctx, argv0, 'offer not found: ' + offerId, argv0 + ' inbox', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
const p = offer.packet.payload
|
const p = offer.packet.payload
|
||||||
const fileName =
|
const fileName =
|
||||||
@@ -650,16 +728,7 @@ async function run(ctx, argv) {
|
|||||||
const db = await bareMeshdropDbRead(ctx)
|
const db = await bareMeshdropDbRead(ctx)
|
||||||
const incoming = db?.incoming?.[offerId]
|
const incoming = db?.incoming?.[offerId]
|
||||||
if (!incoming) {
|
if (!incoming) {
|
||||||
ctx.console.error(
|
return bareP2pError(ctx, argv0, 'offer not accepted locally', argv0 + ' accept ' + offerId, opt)
|
||||||
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 chunks = incoming.chunks && typeof incoming.chunks === 'object' ? incoming.chunks : {}
|
||||||
const total = typeof incoming.chunksTotal === 'number' ? incoming.chunksTotal : 0
|
const total = typeof incoming.chunksTotal === 'number' ? incoming.chunksTotal : 0
|
||||||
@@ -667,20 +736,7 @@ async function run(ctx, argv) {
|
|||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
const part = chunks[i]
|
const part = chunks[i]
|
||||||
if (typeof part !== 'string' || !part) {
|
if (typeof part !== 'string' || !part) {
|
||||||
ctx.console.error(
|
return bareP2pError(ctx, argv0, 'missing chunk ' + i + '/' + total, argv0 + ' status ' + incoming.transferId, opt)
|
||||||
argv0 +
|
|
||||||
': missing chunk ' +
|
|
||||||
i +
|
|
||||||
'/' +
|
|
||||||
total +
|
|
||||||
' (run `' +
|
|
||||||
argv0 +
|
|
||||||
' status ' +
|
|
||||||
incoming.transferId +
|
|
||||||
'` to inspect)'
|
|
||||||
)
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
pieces.push(part)
|
pieces.push(part)
|
||||||
}
|
}
|
||||||
@@ -699,7 +755,7 @@ async function run(ctx, argv) {
|
|||||||
transferId: incoming.transferId,
|
transferId: incoming.transferId,
|
||||||
savedTo: dest
|
savedTo: dest
|
||||||
})
|
})
|
||||||
ctx.console.log('saved ' + offerId + ' -> ' + dest)
|
bareP2pPrint(ctx, 'saved ' + offerId + ' -> ' + dest, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,11 +808,23 @@ async function run(ctx, argv) {
|
|||||||
.filter((x) => !transferId || x.transferId === transferId)
|
.filter((x) => !transferId || x.transferId === transferId)
|
||||||
.sort((a, b) => (b.updatedAtMs || 0) - (a.updatedAtMs || 0))
|
.sort((a, b) => (b.updatedAtMs || 0) - (a.updatedAtMs || 0))
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
ctx.console.log('no transfers')
|
bareP2pPrint(ctx, opt.summary ? { transfers: 0 } : 'no transfers', opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (opt.summary) {
|
||||||
|
bareP2pPrint(
|
||||||
|
ctx,
|
||||||
|
{
|
||||||
|
transfers: items.length,
|
||||||
|
active: items.filter((x) => String(x.status || '').includes('send') || String(x.status || '').includes('receiv')).length
|
||||||
|
},
|
||||||
|
opt
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (const it of items) {
|
for (const it of items) {
|
||||||
ctx.console.log(
|
bareP2pPrint(
|
||||||
|
ctx,
|
||||||
(it.transferId || '?') +
|
(it.transferId || '?') +
|
||||||
' role=' +
|
' role=' +
|
||||||
String(it.role || '?') +
|
String(it.role || '?') +
|
||||||
@@ -766,6 +834,8 @@ async function run(ctx, argv) {
|
|||||||
String(it.chunksReceived || 0) +
|
String(it.chunksReceived || 0) +
|
||||||
'/' +
|
'/' +
|
||||||
String(it.chunksTotal || 0)
|
String(it.chunksTotal || 0)
|
||||||
|
,
|
||||||
|
opt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -774,7 +844,10 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'cancel') {
|
if (sub === 'cancel') {
|
||||||
const transferId = String(args[1] || '').trim()
|
const transferId = String(args[1] || '').trim()
|
||||||
if (!transferId) {
|
if (!transferId) {
|
||||||
ctx.console.error(argv0 + ': cancel requires TRANSFER_ID')
|
return bareP2pError(ctx, argv0, 'cancel requires TRANSFER_ID', argv0 + ' status', opt)
|
||||||
|
}
|
||||||
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes to cancel transfer', opt)
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -784,10 +857,10 @@ async function run(ctx, argv) {
|
|||||||
tr.updatedAtMs = Date.now()
|
tr.updatedAtMs = Date.now()
|
||||||
await bareMeshdropDbWrite(ctx, db)
|
await bareMeshdropDbWrite(ctx, db)
|
||||||
bareP2pSend(ctx, 'meshdrop', 'cancel', { transferId })
|
bareP2pSend(ctx, 'meshdrop', 'cancel', { transferId })
|
||||||
ctx.console.log('cancelled ' + transferId)
|
bareP2pPrint(ctx, 'cancelled ' + transferId, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['offer', 'send-next', 'inbox', 'accept', 'fetch', 'status', 'cancel'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,14 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'p2ping'
|
const argv0 = argv[0] || 'p2ping'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' [LABEL]\n' +
|
'Emit a p2p ping event and print local swarm timing hints.',
|
||||||
'Emit a p2p ping event and print local swarm timing hints.\n' +
|
argv0 + ' [LABEL]',
|
||||||
'See man p2ping.'
|
['hello', '--summary', '--json'],
|
||||||
|
['swarmtop', 'p2ptrace']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -361,6 +473,11 @@ async function run(ctx, argv) {
|
|||||||
localElapsedMs: t1 - t0,
|
localElapsedMs: t1 - t0,
|
||||||
swarmPeers: swarm.peerCount
|
swarmPeers: swarm.peerCount
|
||||||
}
|
}
|
||||||
ctx.console.log(JSON.stringify(out, null, 2))
|
if (opt.summary) {
|
||||||
if (send && send.ok === false) ctx.exitCode = 1
|
bareP2pPrint(ctx, { ok: out.ok, ms: out.localElapsedMs, peers: out.swarmPeers }, opt)
|
||||||
|
} else {
|
||||||
|
bareP2pPrint(ctx, out, opt)
|
||||||
|
}
|
||||||
|
if (send && send.ok === false) bareP2pError(ctx, argv0, String(out.sendReason || 'send failed'), 'swarmdoctor', opt)
|
||||||
|
else bareP2pMaybeNext(ctx, opt, 'p2ptrace p2ping 20')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,15 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'p2ptrace'
|
const argv0 = argv[0] || 'p2ptrace'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' [APP] [N]\n' +
|
'Dump recent p2p events as JSON lines.',
|
||||||
'Dump recent p2p events as JSON lines.\n' +
|
argv0 + ' [APP] [N]',
|
||||||
'APP defaults to all known p2p apps.\n' +
|
['swarmtop 20', 'meshdrop 50', '--summary'],
|
||||||
'See man p2ptrace.'
|
['swarmtop', 'peerctl']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -346,30 +457,21 @@ async function run(ctx, argv) {
|
|||||||
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '200', 10) || 200))
|
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '200', 10) || 200))
|
||||||
const apps = app
|
const apps = app
|
||||||
? [app]
|
? [app]
|
||||||
: [
|
: ['swarmtop','meshdrop','taskmesh','peernote','hypershell-board','p2ping','peerdiscover','peerctl']
|
||||||
'swarmtop',
|
|
||||||
'meshdrop',
|
|
||||||
'taskmesh',
|
|
||||||
'peernote',
|
|
||||||
'hypershell-board',
|
|
||||||
'p2ping',
|
|
||||||
'peerdiscover',
|
|
||||||
'peerctl'
|
|
||||||
]
|
|
||||||
const rows = []
|
const rows = []
|
||||||
for (const a of apps) {
|
for (const a of apps) rows.push(...bareP2pCollectFromHistory(ctx, a, n))
|
||||||
rows.push(...bareP2pCollectFromHistory(ctx, a, n))
|
|
||||||
}
|
|
||||||
rows.sort((a, b) => (a.receivedAtMs || 0) - (b.receivedAtMs || 0))
|
rows.sort((a, b) => (a.receivedAtMs || 0) - (b.receivedAtMs || 0))
|
||||||
|
if (opt.summary) {
|
||||||
|
bareP2pPrint(ctx, { apps: apps.length, events: rows.length, lastAgeMs: rows.length ? Math.max(0, Date.now()-Number(rows[rows.length-1].receivedAtMs||Date.now())) : null }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
for (const r of rows.slice(-n)) {
|
for (const r of rows.slice(-n)) {
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, {
|
||||||
JSON.stringify({
|
|
||||||
atMs: r.receivedAtMs || 0,
|
atMs: r.receivedAtMs || 0,
|
||||||
app: r.packet?.app || '?',
|
app: r.packet?.app || '?',
|
||||||
kind: r.packet?.kind || '?',
|
kind: r.packet?.kind || '?',
|
||||||
from: r.displayName || r.fromPeerKey || '',
|
from: r.displayName || r.fromPeerKey || '',
|
||||||
payload: r.packet?.payload || {}
|
payload: r.packet?.payload || {}
|
||||||
})
|
}, { ...opt, json: true })
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,20 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'peerctl'
|
const argv0 = argv[0] || 'peerctl'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' health\n' +
|
'P2P control plane helper over event envelopes.',
|
||||||
' ' +
|
argv0 + ' health | request ACTION [JSON_PAYLOAD] | history [N]',
|
||||||
argv0 +
|
['health --summary', 'request restart {\"service\":\"swarm\"} --dry-run', 'history 20'],
|
||||||
' request ACTION [JSON_PAYLOAD]\n' +
|
['swarmdoctor', 'p2ptrace']
|
||||||
' ' +
|
)
|
||||||
argv0 +
|
|
||||||
' history [N]\n' +
|
|
||||||
'P2P control plane helper over event envelopes.\n' +
|
|
||||||
'See man peerctl.'
|
|
||||||
)
|
)
|
||||||
if (args.length === 0) ctx.exitCode = 1
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -352,25 +458,19 @@ async function run(ctx, argv) {
|
|||||||
if (sub === 'health') {
|
if (sub === 'health') {
|
||||||
const swarm = await bareP2pReadSwarmSnapshot(ctx)
|
const swarm = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
const doctor = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
const doctor = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
||||||
ctx.console.log(
|
const o = {
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
schema: 1,
|
schema: 1,
|
||||||
swarmPeers: swarm.peerCount,
|
swarmPeers: swarm.peerCount,
|
||||||
health: doctor.health || 'unknown',
|
health: doctor.health || 'unknown',
|
||||||
remediation: doctor.remediation || []
|
remediation: doctor.remediation || []
|
||||||
},
|
}
|
||||||
null,
|
bareP2pPrint(ctx, opt.summary ? { health: o.health, peers: o.swarmPeers } : o, opt)
|
||||||
2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'request') {
|
if (sub === 'request') {
|
||||||
const action = String(args[1] || '').trim()
|
const action = String(args[1] || '').trim()
|
||||||
if (!action) {
|
if (!action) {
|
||||||
ctx.console.error(argv0 + ': request requires ACTION')
|
bareP2pError(ctx, argv0, 'request requires ACTION', argv0 + ' --help', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let payload = {}
|
let payload = {}
|
||||||
@@ -378,34 +478,46 @@ async function run(ctx, argv) {
|
|||||||
try {
|
try {
|
||||||
payload = JSON.parse(args.slice(2).join(' '))
|
payload = JSON.parse(args.slice(2).join(' '))
|
||||||
} catch {
|
} catch {
|
||||||
ctx.console.error(argv0 + ': invalid JSON payload')
|
bareP2pError(ctx, argv0, 'invalid JSON payload', argv0 + ' request ACTION {\"key\":\"value\"}', opt)
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (opt.dryRun) {
|
||||||
|
bareP2pPrint(ctx, { ok: true, dryRun: true, action, payload }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes to send control request', opt)
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
const r = bareP2pSend(ctx, 'peerctl', 'request', {
|
const r = bareP2pSend(ctx, 'peerctl', 'request', {
|
||||||
requestId: bareP2pId('ctl'),
|
requestId: bareP2pId('ctl'),
|
||||||
action,
|
action,
|
||||||
payload
|
payload
|
||||||
})
|
})
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
|
else bareP2pMaybeNext(ctx, opt, argv0 + ' history 20')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'history') {
|
if (sub === 'history') {
|
||||||
const n = Math.max(1, Math.min(1000, parseInt(args[1] || '50', 10) || 50))
|
const n = Math.max(1, Math.min(1000, parseInt(args[1] || '50', 10) || 50))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'peerctl', 2000).slice(-n)
|
const rows = bareP2pCollectFromHistory(ctx, 'peerctl', 2000).slice(-n)
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
ctx.console.log(
|
bareP2pPrint(
|
||||||
|
ctx,
|
||||||
'[' +
|
'[' +
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
'] ' +
|
'] ' +
|
||||||
String(row.packet?.kind || '?') +
|
String(row.packet?.kind || '?') +
|
||||||
' ' +
|
' ' +
|
||||||
JSON.stringify(row.packet?.payload || {})
|
JSON.stringify(row.packet?.payload || {})
|
||||||
|
,
|
||||||
|
opt
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['health', 'request', 'history'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,17 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'peerdiscover'
|
const argv0 = argv[0] || 'peerdiscover'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' announce SERVICE [META]\n' +
|
'Peer service announce/list over p2p event history.',
|
||||||
' ' +
|
argv0 + ' announce SERVICE [META] | list [N]',
|
||||||
argv0 +
|
['announce filesync "v1"', 'list 20 --summary'],
|
||||||
' list [N]\n' +
|
['p2ptrace', 'swarmmap']
|
||||||
'Peer service announce/list over p2p event history.\n' +
|
)
|
||||||
'See man peerdiscover.'
|
|
||||||
)
|
)
|
||||||
if (args.length === 0) ctx.exitCode = 1
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -348,37 +457,24 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'announce') {
|
if (sub === 'announce') {
|
||||||
const service = String(args[1] || '').trim()
|
const service = String(args[1] || '').trim()
|
||||||
if (!service) {
|
if (!service) return bareP2pError(ctx, argv0, 'announce requires SERVICE', argv0 + ' --help', opt)
|
||||||
ctx.console.error(argv0 + ': announce requires SERVICE')
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const meta = args.slice(2).join(' ').trim()
|
const meta = args.slice(2).join(' ').trim()
|
||||||
const r = bareP2pSend(ctx, 'peerdiscover', 'service.announce', {
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, service, meta }, opt)
|
||||||
service,
|
const r = bareP2pSend(ctx, 'peerdiscover', 'service.announce', { service, meta, id: bareP2pId('svc') })
|
||||||
meta,
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
id: bareP2pId('svc')
|
else bareP2pMaybeNext(ctx, opt, argv0 + ' list 20')
|
||||||
})
|
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const n = Math.max(1, Math.min(500, parseInt(args[1] || '50', 10) || 50))
|
const n = Math.max(1, Math.min(500, parseInt(args[1] || '50', 10) || 50))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'peerdiscover', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'peerdiscover', 2000).filter((r) => r.packet?.kind === 'service.announce').slice(-n)
|
||||||
.filter((r) => r.packet?.kind === 'service.announce')
|
if (opt.summary) return bareP2pPrint(ctx, { services: rows.length }, opt)
|
||||||
.slice(-n)
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const p = row.packet?.payload || {}
|
const p = row.packet?.payload || {}
|
||||||
ctx.console.log(
|
bareP2pPrint(ctx, '[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + String(p.service || 'unknown') + (p.meta ? ' ' + String(p.meta) : ''), opt)
|
||||||
'[' +
|
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
|
||||||
'] ' +
|
|
||||||
String(p.service || 'unknown') +
|
|
||||||
(p.meta ? ' ' + String(p.meta) : '')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['announce', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,17 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'peernote'
|
const argv0 = argv[0] || 'peernote'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' add TEXT\n' +
|
'Shared p2p note stream over bare-p2p envelopes.',
|
||||||
' ' +
|
argv0 + ' add TEXT | list [N]',
|
||||||
argv0 +
|
['add "hello cluster"', 'list 20 --summary'],
|
||||||
' list [N]\n' +
|
['taskmesh', 'p2ptrace']
|
||||||
'Shared p2p note stream over bare-p2p envelopes.\n' +
|
)
|
||||||
'See man peernote.'
|
|
||||||
)
|
)
|
||||||
if (args.length === 0) ctx.exitCode = 1
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -348,31 +457,25 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'add') {
|
if (sub === 'add') {
|
||||||
const text = args.slice(1).join(' ').trim()
|
const text = args.slice(1).join(' ').trim()
|
||||||
if (!text) {
|
if (!text) return bareP2pError(ctx, argv0, 'add requires text', argv0 + ' add "text"', opt)
|
||||||
ctx.console.error(argv0 + ': add requires text')
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, text }, opt)
|
||||||
ctx.exitCode = 1
|
const r = bareP2pSend(ctx, 'peernote', 'note.add', { noteId: bareP2pId('note'), text })
|
||||||
return
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
}
|
else bareP2pMaybeNext(ctx, opt, argv0 + ' list 20')
|
||||||
const r = bareP2pSend(ctx, 'peernote', 'note.add', {
|
|
||||||
noteId: bareP2pId('note'),
|
|
||||||
text
|
|
||||||
})
|
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const n = Math.max(1, Math.min(300, parseInt(args[1] || '30', 10) || 30))
|
const n = Math.max(1, Math.min(300, parseInt(args[1] || '30', 10) || 30))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'peernote', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'peernote', 2000).filter((r) => r.packet?.kind === 'note.add').slice(-n)
|
||||||
.filter((r) => r.packet?.kind === 'note.add')
|
if (opt.summary) return bareP2pPrint(ctx, { notes: rows.length }, opt)
|
||||||
.slice(-n)
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const p = row.packet?.payload || {}
|
const p = row.packet?.payload || {}
|
||||||
const text = typeof p.text === 'string' ? p.text : ''
|
const text = typeof p.text === 'string' ? p.text : ''
|
||||||
const who = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
const who = row.displayName || row.fromPeerKey.slice(0, 10) || 'peer'
|
||||||
ctx.console.log('[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + who + ': ' + text)
|
bareP2pPrint(ctx, '[' + bareP2pFmtClock(row.receivedAtMs) + '] ' + who + ': ' + text, opt)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['add', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -570,6 +570,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -920,18 +1028,26 @@ async function bareP2pRunSimpleTui(ctx, opts) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'routeview'
|
const argv0 = argv[0] || 'routeview'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' + argv0 + '\n' + 'Route and relay visibility TUI.\n' + 'See man routeview.'
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'Route and relay visibility TUI.',
|
||||||
|
argv0 + ' [--summary] [--json]',
|
||||||
|
['--summary', '--json'],
|
||||||
|
['holepunch-view', 'swarmdoctor']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
const r = await bareP2pReadProcJson(ctx, '/proc/bare_os/route_summary.json')
|
const r = await bareP2pReadProcJson(ctx, '/proc/bare_os/route_summary.json')
|
||||||
ctx.console.log(JSON.stringify(r, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: r.peers ?? null, direct: r.directLikely ?? null, relay: r.relayLikely ?? null } : r, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
|
|||||||
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,20 +438,24 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'swarmdoctor'
|
const argv0 = argv[0] || 'swarmdoctor'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
'\n' +
|
'Print swarm diagnosis from /proc/bare_os/swarm_doctor.json.',
|
||||||
'Print swarm diagnosis from /proc/bare_os/swarm_doctor.json.\n' +
|
argv0 + ' [--summary] [--json]',
|
||||||
'See man swarmdoctor.'
|
['--summary', '--json'],
|
||||||
|
['swarmtop', 'peerctl']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const doc = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
const doc = await bareP2pReadProcJson(ctx, '/proc/bare_os/swarm_doctor.json')
|
||||||
ctx.console.log(JSON.stringify(doc, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { health: doc.health || 'unknown', findings: (doc.findings && doc.findings.length) || 0 } : doc, opt)
|
||||||
if (doc && typeof doc === 'object' && doc.health && doc.health !== 'ok') {
|
if (doc && typeof doc === 'object' && doc.health && doc.health !== 'ok') {
|
||||||
|
if (!opt.quiet) ctx.console.error('hint: check routeview and holepunch-view for network posture')
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -570,6 +570,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -920,18 +1028,25 @@ async function bareP2pRunSimpleTui(ctx, opts) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'swarmmap'
|
const argv0 = argv[0] || 'swarmmap'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' + argv0 + '\n' + 'Swarm topology map TUI.\n' + 'See man swarmmap.'
|
bareP2pHelpText(
|
||||||
|
argv0,
|
||||||
|
'Swarm topology map TUI.',
|
||||||
|
argv0 + ' [--summary] [--json]',
|
||||||
|
['--summary', '--json'],
|
||||||
|
['swarmtop', 'peerdiscover']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const stdin = ctx.replStdin
|
const stdin = ctx.replStdin
|
||||||
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
const isTTY = Boolean(stdin && /** @type {{ isTTY?: boolean }} */ (stdin).isTTY)
|
||||||
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
if (!isTTY) {
|
if (!isTTY || opt.json || opt.summary) {
|
||||||
ctx.console.log(JSON.stringify(snap, null, 2))
|
bareP2pPrint(ctx, opt.summary ? { peers: snap.peerCount, topics: snap.topicCount } : snap, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await bareP2pRunSimpleTui(ctx, {
|
await bareP2pRunSimpleTui(ctx, {
|
||||||
|
|||||||
@@ -570,6 +570,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -920,36 +1028,46 @@ async function bareP2pRunSimpleTui(ctx, opts) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'swarmtop'
|
const argv0 = argv[0] || 'swarmtop'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help')) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' [watch | events [N] | ping [LABEL]]\n' +
|
'P2P fleet dashboard over Bare OS chat + /proc swarm data.',
|
||||||
'P2P fleet dashboard over Bare OS chat + /proc swarm data.\n' +
|
argv0 + ' [watch | events [N] | ping [LABEL]]',
|
||||||
'No args on a TTY opens full-screen swarmtop.\n' +
|
['ping hello', 'events 20', 'watch --json'],
|
||||||
'See man swarmtop.'
|
['swarmdoctor', 'swarmmap', 'p2ptrace']
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
bareP2pFirstRunHint(ctx, 'swarmtop', "run 'swarmdoctor' when peer rows look incomplete")
|
||||||
|
|
||||||
const sub = args[0] || ''
|
const sub = args[0] || ''
|
||||||
if (sub === 'ping') {
|
if (sub === 'ping') {
|
||||||
const label = args.slice(1).join(' ').trim() || 'hello'
|
const label = args.slice(1).join(' ').trim() || 'hello'
|
||||||
|
if (opt.dryRun) {
|
||||||
|
bareP2pPrint(ctx, { ok: true, dryRun: true, action: 'ping', label }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
const r = bareP2pSend(ctx, 'swarmtop', 'ping', {
|
const r = bareP2pSend(ctx, 'swarmtop', 'ping', {
|
||||||
id: bareP2pId('ping'),
|
id: bareP2pId('ping'),
|
||||||
label
|
label
|
||||||
})
|
})
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
|
else bareP2pMaybeNext(ctx, opt, 'swarmtop events 20')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (sub === 'events') {
|
if (sub === 'events') {
|
||||||
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '30', 10) || 30))
|
const n = Math.max(1, Math.min(2000, parseInt(args[1] || '30', 10) || 30))
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'swarmtop', n)
|
const rows = bareP2pCollectFromHistory(ctx, 'swarmtop', n)
|
||||||
|
const out = []
|
||||||
for (const row of rows.slice(-n)) {
|
for (const row of rows.slice(-n)) {
|
||||||
const pkt = row.packet || {}
|
const pkt = row.packet || {}
|
||||||
const payload = pkt.payload && typeof pkt.payload === 'object' ? pkt.payload : {}
|
const payload = pkt.payload && typeof pkt.payload === 'object' ? pkt.payload : {}
|
||||||
ctx.console.log(
|
out.push(
|
||||||
'[' +
|
'[' +
|
||||||
bareP2pFmtClock(row.receivedAtMs) +
|
bareP2pFmtClock(row.receivedAtMs) +
|
||||||
'] ' +
|
'] ' +
|
||||||
@@ -958,6 +1076,12 @@ async function run(ctx, argv) {
|
|||||||
JSON.stringify(payload)
|
JSON.stringify(payload)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (!out.length) out.push('(none)')
|
||||||
|
if (opt.summary) {
|
||||||
|
bareP2pPrint(ctx, { app: 'swarmtop', events: out.length }, opt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const ln of out) bareP2pPrint(ctx, ln, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,6 +1129,7 @@ async function run(ctx, argv) {
|
|||||||
lines.push('Peers:')
|
lines.push('Peers:')
|
||||||
const peers = Array.isArray(snap.peers) ? snap.peers : []
|
const peers = Array.isArray(snap.peers) ? snap.peers : []
|
||||||
if (!peers.length) lines.push(' (no peers reported)')
|
if (!peers.length) lines.push(' (no peers reported)')
|
||||||
|
let unknownCount = 0
|
||||||
for (const p of peers.slice(0, 12)) {
|
for (const p of peers.slice(0, 12)) {
|
||||||
const key =
|
const key =
|
||||||
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
|
p && typeof p === 'object' && typeof p.remotePublicKey === 'string'
|
||||||
@@ -1014,12 +1139,19 @@ async function run(ctx, argv) {
|
|||||||
p && typeof p === 'object' && typeof p.state === 'string'
|
p && typeof p === 'object' && typeof p.state === 'string'
|
||||||
? p.state
|
? p.state
|
||||||
: 'connected'
|
: 'connected'
|
||||||
const addr =
|
const endpointObj =
|
||||||
p && typeof p === 'object' && p.endpoint && typeof p.endpoint === 'object'
|
p && typeof p === 'object' && p.endpoint && typeof p.endpoint === 'object'
|
||||||
? String(p.endpoint.address || p.remoteAddress || '?') +
|
? p.endpoint
|
||||||
':' +
|
: null
|
||||||
String(p.endpoint.port || p.remotePort || '?')
|
const addrRaw = String(
|
||||||
: String(p?.remoteAddress || '?') + ':' + String(p?.remotePort || '?')
|
(endpointObj && endpointObj.address) ||
|
||||||
|
(p && typeof p === 'object' ? p.remoteAddress || '' : '')
|
||||||
|
).trim()
|
||||||
|
const portRaw = String(
|
||||||
|
(endpointObj && endpointObj.port) ||
|
||||||
|
(p && typeof p === 'object' ? p.remotePort || '' : '')
|
||||||
|
).trim()
|
||||||
|
const addr = addrRaw && portRaw ? addrRaw + ':' + portRaw : 'unavailable'
|
||||||
const osHint =
|
const osHint =
|
||||||
p &&
|
p &&
|
||||||
typeof p === 'object' &&
|
typeof p === 'object' &&
|
||||||
@@ -1028,12 +1160,27 @@ async function run(ctx, argv) {
|
|||||||
p.peerOs.osHint
|
p.peerOs.osHint
|
||||||
? String(p.peerOs.osHint)
|
? String(p.peerOs.osHint)
|
||||||
: 'unknown'
|
: 'unknown'
|
||||||
|
const osConfidence = osHint === 'unknown' ? 'unknown' : 'reported'
|
||||||
|
if (addr === 'unavailable' || osHint === 'unknown') unknownCount++
|
||||||
lines.push(' pubkey: ' + key)
|
lines.push(' pubkey: ' + key)
|
||||||
lines.push(' state=' + state + ' endpoint=' + addr + ' os=' + osHint)
|
lines.push(
|
||||||
|
' state=' +
|
||||||
|
state +
|
||||||
|
' endpoint=' +
|
||||||
|
addr +
|
||||||
|
' os=' +
|
||||||
|
osHint +
|
||||||
|
' osConfidence=' +
|
||||||
|
osConfidence
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
lines.push(' peerData diagnostics: incomplete=' + unknownCount + '/' + peers.length)
|
||||||
lines.push('')
|
lines.push('')
|
||||||
lines.push('Recent events:')
|
lines.push('Recent events:')
|
||||||
if (!events.length) lines.push(' (none)')
|
if (!events.length) {
|
||||||
|
lines.push(' (none)')
|
||||||
|
lines.push(' event ingest: no app events seen yet; try r, then swarmtop ping hello')
|
||||||
|
}
|
||||||
for (const ev of events) {
|
for (const ev of events) {
|
||||||
lines.push(
|
lines.push(
|
||||||
' [' +
|
' [' +
|
||||||
@@ -1050,10 +1197,10 @@ async function run(ctx, argv) {
|
|||||||
|
|
||||||
if (sub === 'watch') {
|
if (sub === 'watch') {
|
||||||
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
const snap = await bareP2pReadSwarmSnapshot(ctx)
|
||||||
ctx.console.log(JSON.stringify(snap, null, 2))
|
bareP2pPrint(ctx, snap, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['watch', 'events', 'ping'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,114 @@ function bareP2pId(prefix) {
|
|||||||
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
return prefix + '-' + bareP2pNowMs().toString(36) + '-' + rnd.toString(36)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bareP2pCommonExamples(argv0, rows) {
|
||||||
|
const out = []
|
||||||
|
for (const r of rows || []) out.push(' ' + argv0 + ' ' + r)
|
||||||
|
return out.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pHelpText(argv0, summary, usage, examples, seeAlso) {
|
||||||
|
const lines = []
|
||||||
|
lines.push('usage: ' + usage)
|
||||||
|
lines.push(summary)
|
||||||
|
lines.push('')
|
||||||
|
lines.push('common options: --help --json --quiet --summary --timeout MS --no-color')
|
||||||
|
if (examples && examples.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('examples:')
|
||||||
|
lines.push(bareP2pCommonExamples(argv0, examples))
|
||||||
|
}
|
||||||
|
if (seeAlso && seeAlso.length) {
|
||||||
|
lines.push('')
|
||||||
|
lines.push('see also: ' + seeAlso.join(', '))
|
||||||
|
}
|
||||||
|
lines.push('')
|
||||||
|
lines.push('troubleshooting: if peer data is empty, run `swarmtop` then `swarmdoctor`.')
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pParseCommonFlags(args) {
|
||||||
|
const rest = []
|
||||||
|
const opt = {
|
||||||
|
help: false,
|
||||||
|
json: false,
|
||||||
|
quiet: false,
|
||||||
|
summary: false,
|
||||||
|
timeoutMs: 4000,
|
||||||
|
noColor: false,
|
||||||
|
dryRun: false,
|
||||||
|
yes: false
|
||||||
|
}
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
const a = String(args[i] || '')
|
||||||
|
if (a === '-h' || a === '--help') opt.help = true
|
||||||
|
else if (a === '--json') opt.json = true
|
||||||
|
else if (a === '--quiet') opt.quiet = true
|
||||||
|
else if (a === '--summary') opt.summary = true
|
||||||
|
else if (a === '--dry-run') opt.dryRun = true
|
||||||
|
else if (a === '--yes' || a === '-y') opt.yes = true
|
||||||
|
else if (a === '--no-color') opt.noColor = true
|
||||||
|
else if (a === '--timeout') {
|
||||||
|
const n = parseInt(args[i + 1] || '4000', 10)
|
||||||
|
if (Number.isFinite(n)) opt.timeoutMs = Math.max(250, Math.min(120000, n))
|
||||||
|
i++
|
||||||
|
} else rest.push(a)
|
||||||
|
}
|
||||||
|
return { opt, rest }
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pPrint(ctx, data, opt) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (opt && (opt.json || typeof data !== 'string')) {
|
||||||
|
ctx.console.log(typeof data === 'string' ? JSON.stringify({ message: data }) : JSON.stringify(data, null, 2))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.log(String(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pError(ctx, argv0, msg, next, opt) {
|
||||||
|
if (opt && opt.quiet) {
|
||||||
|
ctx.exitCode = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.console.error(argv0 + ': ' + msg + (next ? ' · try: ' + next : ''))
|
||||||
|
ctx.exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pSuggestSubcommand(sub, known) {
|
||||||
|
const s = String(sub || '')
|
||||||
|
if (!s) return ''
|
||||||
|
let best = ''
|
||||||
|
let bestScore = 1e9
|
||||||
|
for (const k of known || []) {
|
||||||
|
const kk = String(k || '')
|
||||||
|
const d = Math.abs(kk.length - s.length) + (kk[0] === s[0] ? 0 : 2)
|
||||||
|
if (d < bestScore) {
|
||||||
|
bestScore = d
|
||||||
|
best = kk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestScore <= 4 ? best : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pFirstRunHint(ctx, app, hint) {
|
||||||
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
|
if (env.BARE_P2P_NO_HINTS === '1' || env.BARE_P2P_NO_HINTS === 'true') return
|
||||||
|
const key = '__bare_p2p_hint_' + app
|
||||||
|
if (ctx[key]) return
|
||||||
|
ctx[key] = true
|
||||||
|
try {
|
||||||
|
ctx.console.log('hint: ' + hint)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bareP2pMaybeNext(ctx, opt, next) {
|
||||||
|
if (opt && opt.quiet) return
|
||||||
|
if (next) ctx.console.log('next: ' + next)
|
||||||
|
}
|
||||||
|
|
||||||
function bareP2pHome(ctx) {
|
function bareP2pHome(ctx) {
|
||||||
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
const env = ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
||||||
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
const home = typeof env.HOME === 'string' && env.HOME ? env.HOME : '/home/guest'
|
||||||
@@ -330,20 +438,18 @@ async function bareP2pReadProcJson(ctx, path) {
|
|||||||
|
|
||||||
async function run(ctx, argv) {
|
async function run(ctx, argv) {
|
||||||
const argv0 = argv[0] || 'taskmesh'
|
const argv0 = argv[0] || 'taskmesh'
|
||||||
const args = argv.slice(1)
|
const parsed = bareP2pParseCommonFlags(argv.slice(1))
|
||||||
if (args.includes('-h') || args.includes('--help') || args.length === 0) {
|
const args = parsed.rest
|
||||||
|
const opt = parsed.opt
|
||||||
|
if (opt.help || args.length === 0) {
|
||||||
ctx.console.log(
|
ctx.console.log(
|
||||||
'usage: ' +
|
bareP2pHelpText(
|
||||||
argv0 +
|
argv0,
|
||||||
' add TEXT\n' +
|
'P2P task board over append-only bare-p2p events.',
|
||||||
' ' +
|
argv0 + ' add TEXT | done TASK_ID | list [open|all]',
|
||||||
argv0 +
|
['add "ship release"', 'list all', 'done task-abc'],
|
||||||
' done TASK_ID\n' +
|
['peernote', 'p2ptrace']
|
||||||
' ' +
|
)
|
||||||
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
|
if (args.length === 0) ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
@@ -352,63 +458,56 @@ async function run(ctx, argv) {
|
|||||||
const sub = args[0]
|
const sub = args[0]
|
||||||
if (sub === 'add') {
|
if (sub === 'add') {
|
||||||
const text = args.slice(1).join(' ').trim()
|
const text = args.slice(1).join(' ').trim()
|
||||||
if (!text) {
|
if (!text) return bareP2pError(ctx, argv0, 'add requires text', argv0 + ' add "text"', opt)
|
||||||
ctx.console.error(argv0 + ': add requires text')
|
|
||||||
ctx.exitCode = 1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const taskId = bareP2pId('task')
|
const taskId = bareP2pId('task')
|
||||||
const r = bareP2pSend(ctx, 'taskmesh', 'task.add', {
|
if (opt.dryRun) return bareP2pPrint(ctx, { ok: true, dryRun: true, taskId, text }, opt)
|
||||||
taskId,
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.add', { taskId, text })
|
||||||
text
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
})
|
else {
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
bareP2pPrint(ctx, 'added ' + taskId, opt)
|
||||||
else ctx.console.log('added ' + taskId)
|
bareP2pMaybeNext(ctx, opt, argv0 + ' list')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'done') {
|
if (sub === 'done') {
|
||||||
const taskId = String(args[1] || '').trim()
|
const taskId = String(args[1] || '').trim()
|
||||||
if (!taskId) {
|
if (!taskId) return bareP2pError(ctx, argv0, 'done requires TASK_ID', argv0 + ' list', opt)
|
||||||
ctx.console.error(argv0 + ': done requires TASK_ID')
|
if (!opt.yes) {
|
||||||
|
bareP2pPrint(ctx, 'confirmation required: pass --yes to mark done', opt)
|
||||||
ctx.exitCode = 1
|
ctx.exitCode = 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const r = bareP2pSend(ctx, 'taskmesh', 'task.done', { taskId })
|
const r = bareP2pSend(ctx, 'taskmesh', 'task.done', { taskId })
|
||||||
if (r && r.ok === false) ctx.exitCode = 1
|
if (r && r.ok === false) bareP2pError(ctx, argv0, String(r.reason || 'send failed'), 'swarmdoctor', opt)
|
||||||
else ctx.console.log('completed ' + taskId)
|
else bareP2pPrint(ctx, 'completed ' + taskId, opt)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub === 'list') {
|
if (sub === 'list') {
|
||||||
const mode = args[1] === 'all' ? 'all' : 'open'
|
const mode = args[1] === 'all' ? 'all' : 'open'
|
||||||
const rows = bareP2pCollectFromHistory(ctx, 'taskmesh', 2000)
|
const rows = bareP2pCollectFromHistory(ctx, 'taskmesh', 2000)
|
||||||
/** @type {Map<string, { taskId: string, text: string, done: boolean, atMs: number }>} */
|
|
||||||
const board = new Map()
|
const board = new Map()
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const kind = row.packet?.kind
|
const kind = row.packet?.kind
|
||||||
const p = row.packet?.payload
|
const p = row.packet?.payload
|
||||||
if (!p || typeof p !== 'object') continue
|
if (!p || typeof p !== 'object') continue
|
||||||
if (kind === 'task.add' && typeof p.taskId === 'string') {
|
if (kind === 'task.add' && typeof p.taskId === 'string') {
|
||||||
board.set(p.taskId, {
|
board.set(p.taskId, { taskId: p.taskId, text: typeof p.text === 'string' ? p.text : '', done: false, atMs: row.receivedAtMs })
|
||||||
taskId: p.taskId,
|
|
||||||
text: typeof p.text === 'string' ? p.text : '',
|
|
||||||
done: false,
|
|
||||||
atMs: row.receivedAtMs
|
|
||||||
})
|
|
||||||
} else if (kind === 'task.done' && typeof p.taskId === 'string') {
|
} else if (kind === 'task.done' && typeof p.taskId === 'string') {
|
||||||
const cur = board.get(p.taskId)
|
const cur = board.get(p.taskId)
|
||||||
if (cur) cur.done = true
|
if (cur) cur.done = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const all = [...board.values()].sort((a, b) => a.atMs - b.atMs)
|
const all = [...board.values()].sort((a, b) => a.atMs - b.atMs)
|
||||||
|
if (opt.summary) return bareP2pPrint(ctx, { tasks: all.length, open: all.filter((x)=>!x.done).length }, opt)
|
||||||
for (const t of all) {
|
for (const t of all) {
|
||||||
if (mode !== 'all' && t.done) continue
|
if (mode !== 'all' && t.done) continue
|
||||||
ctx.console.log((t.done ? '[x] ' : '[ ] ') + t.taskId + ' ' + t.text)
|
bareP2pPrint(ctx, (t.done ? '[x] ' : '[ ] ') + t.taskId + ' ' + t.text, opt)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.console.error(argv0 + ': unsupported subcommand')
|
const sug = bareP2pSuggestSubcommand(sub, ['add', 'done', 'list'])
|
||||||
ctx.exitCode = 1
|
bareP2pError(ctx, argv0, 'unsupported subcommand' + (sug ? ' (did you mean ' + sug + '?)' : ''), argv0 + ' --help', opt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema": 2,
|
"schema": 2,
|
||||||
"profileId": "bare-os-posix-like",
|
"profileId": "bare-os-posix-like",
|
||||||
"generatedAt": "2026-04-26T12:27:28.060Z",
|
"generatedAt": "2026-04-26T12:40:06.854Z",
|
||||||
"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": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"atMs": 1777206448060,
|
"atMs": 1777207206853,
|
||||||
"commands": [
|
"commands": [
|
||||||
"agent",
|
"agent",
|
||||||
"arch",
|
"arch",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user