#!/usr/bin/env bare 'use strict' const fs = require('bare-fs') const path = require('bare-path') const { AgentClient, HeadlessSession, PearcordMeshCluster, resolveAgentctlAddress } = require('..') function usage () { console.log(`pearcord-agentctl — control a running Pearcord worker or run headless IPC Usage: agentctl ping [--host HOST] [--port PORT] agentctl view [--light] [--host HOST] [--port PORT] agentctl ipc '' [--host HOST] [--port PORT] agentctl wait '' [--timeout MS] [--host HOST] [--port PORT] agentctl wait-event '' [--timeout MS] [--since MS] agentctl log-tail [--lines N] [--host HOST] [--port PORT] agentctl run [--timeout MS] [--host HOST] [--port PORT] agentctl headless run agentctl headless mesh run [--peers N] agentctl headless ipc '' agentctl mesh run [--peers N] Environment: PEARCORD_AGENTCTL=1 Enable TCP server in apps/pearcord worker PEARCORD_AGENTCTL_HOST Default 127.0.0.1 PEARCORD_AGENTCTL_PORT Default 39482 PEARCORD_AGENTCTL_TOKEN Optional shared secret for TCP ops PEARCORD_STORAGE Storage root (~/.config/pearcord) Start Pearcord with agentctl: PEARCORD_AGENTCTL=1 pear run `) } function parseArgs (argv) { const out = { _: [], host: null, port: null, light: false, timeout: 15000, lines: 40 } for (let i = 2; i < argv.length; i++) { const a = argv[i] if (a === '--host') out.host = argv[++i] else if (a === '--port') out.port = Number(argv[++i]) else if (a === '--light') out.light = true else if (a === '--timeout') out.timeout = Number(argv[++i]) else if (a === '--lines') out.lines = Number(argv[++i]) else if (a === '-h' || a === '--help') out.help = true else out._.push(a) } return out } function clientFromArgs (args) { const def = resolveAgentctlAddress() return new AgentClient({ host: args.host || def.host, port: args.port || def.port }) } async function main () { const args = parseArgs(process.argv) if (args.help || !args._.length) { usage() process.exit(args.help ? 0 : 1) } const cmd = args._[0] if (cmd === 'headless') { const sub = args._[1] if (sub === 'mesh') { const meshSub = args._[2] if (meshSub !== 'run') throw new Error('use: headless mesh run ') const file = args._[3] if (!file) throw new Error('scenario.json path required') const cluster = new PearcordMeshCluster({ peerCount: 3 }) try { const result = await cluster.runScenario(path.resolve(file)) console.log(JSON.stringify({ ok: true, result }, null, 2)) } finally { await cluster.close() } return } const session = new HeadlessSession() await session.start() try { if (sub === 'run') { const file = args._[2] if (!file) throw new Error('scenario.json path required') const results = await session.runScenario(path.resolve(file)) console.log(JSON.stringify({ ok: true, steps: results.length, view: session.lastView }, null, 2)) return } if (sub === 'ipc') { const raw = args._[2] if (!raw) throw new Error('ipc json required') const payload = JSON.parse(raw) const view = await session.ipc(payload) console.log(JSON.stringify({ ok: true, view }, null, 2)) return } throw new Error(`unknown headless subcommand: ${sub}`) } finally { await session.close() } } if (cmd === 'mesh') { const meshSub = args._[1] if (meshSub !== 'run') throw new Error('use: mesh run ') const file = args._[2] if (!file) throw new Error('scenario.json path required') const cluster = new PearcordMeshCluster({ peerCount: 3 }) try { const result = await cluster.runScenario(path.resolve(file)) console.log(JSON.stringify({ ok: true, result }, null, 2)) } finally { await cluster.close() } return } const client = clientFromArgs(args) try { if (cmd === 'ping') { const res = await client.ping() console.log(JSON.stringify(res, null, 2)) return } if (cmd === 'view') { const view = await client.view(args.light) console.log(JSON.stringify({ ok: true, view }, null, 2)) return } if (cmd === 'ipc') { const raw = args._[1] if (!raw) throw new Error('ipc json required') const payload = JSON.parse(raw) const res = await client.ipc(payload) console.log(JSON.stringify(res, null, 2)) return } if (cmd === 'wait') { const raw = args._[1] if (!raw) throw new Error('match json required') const match = JSON.parse(raw) const view = await client.wait(match, args.timeout, args.light) console.log(JSON.stringify({ ok: true, view }, null, 2)) return } if (cmd === 'log-tail') { const res = await client.logTail(args.lines) console.log(JSON.stringify(res, null, 2)) return } if (cmd === 'wait-event') { const raw = args._[1] if (!raw) throw new Error('match json required') const match = JSON.parse(raw) const event = await client.waitEvent(match, args.timeout, 0) console.log(JSON.stringify({ ok: true, event }, null, 2)) return } if (cmd === 'run') { const file = args._[1] if (!file) throw new Error('scenario.json path required') const res = await client.run(path.resolve(file), args.timeout || 120000) console.log(JSON.stringify(res, null, 2)) return } throw new Error(`unknown command: ${cmd}`) } finally { client.close() } } main().catch((err) => { console.error(err?.message || err) process.exit(1) })