/** * Flying Jib CLI — Bare entry (ADR-0013). * * Usage: * bare bin.mjs create * bare bin.mjs list * bare bin.mjs start [--port 25565] * bare bin.mjs host [--port 25565] # start + share invite * bare bin.mjs join [--port 25565] # client tunnel * bare bin.mjs status */ import { createRequire } from 'module' import { command, flag, arg, summary, header, footer } from 'paparam' import path from 'path' import process from 'process' import os from 'os' import pkg from './package.json' with { type: 'json' } import App from './app.js' // createRequire needs a file: URL in Node; under bare-pack standalone, import.meta.url // may be app.bundle — fall back to a dummy path for require() of CJS shims. let require try { require = createRequire(import.meta.url) } catch { require = createRequire('file:///flying-jib/bin.mjs') } const appName = pkg.productName || pkg.name const isBare = typeof Bare !== 'undefined' const argv0 = isBare ? Bare.argv[0] : process.argv[0] const base = path.basename(argv0) const isDev = base === 'bare' || base === 'node' || /bare-runtime/.test(argv0) const rawArgv = isBare ? Bare.argv.slice(isDev ? 2 : 1) : process.argv.slice(2) function defaultTmpDir() { try { if (typeof os.tmpdir === 'function') return os.tmpdir() } catch { /* ignore */ } return process.env.TMPDIR || process.env.TEMP || process.env.TMP || '/tmp' } function resolveStorage(flagStorage) { if (flagStorage) return path.resolve(flagStorage) const tmp = defaultTmpDir() if (isBare) { try { const { persistent } = require('bare-storage') return isDev ? path.join(tmp, 'pear', appName) : path.join(persistent(), appName) } catch { return path.join(tmp, 'pear', appName) } } return path.join(tmp, 'pear', appName) } function exit(code) { if (isBare) Bare.exit(code) else process.exit(code) } function makeApp(storage, updates = false, displayName, mcUsername) { return new App({ dir: storage, appPath: isDev ? null : argv0, updates, version: pkg.version, upgrade: pkg.upgrade, name: appName, displayName: displayName || 'player', mcUsername: mcUsername || displayName || null }) } /** Wire chat/presence logging + optional stdin chat lines for host/join */ function attachPeerUi(app) { app.on('chat', (m) => { const tag = m.local ? 'you' : m.from console.log(`[chat] <${tag}> ${m.text}`) }) app.on('peer-join', (m) => { console.log(`[peers] joined ${m.peerId.slice(0, 12)}…`) }) app.on('peer-leave', (m) => { console.log(`[peers] left ${m.peerId.slice(0, 12)}…`) }) app.on('peers', (list) => { if (list.length) { console.log(`[peers] online: ${list.map((p) => p.name).join(', ')}`) } }) app.on('border', (evt) => { if (evt.type === 'warn') { console.log( `[mesh] border warn ${evt.player?.username} → ${evt.neighbor?.name || evt.neighbor?.regionId || 'edge'}` ) } }) app.on('migrate', (evt) => { console.log( `[mesh] migrate ${evt.player?.username || evt.username} → ${evt.neighbor?.name || evt.neighbor?.regionId}` ) }) app.on('migrated', (evt) => { console.log(`[mesh] tunnel now 127.0.0.1:${evt.localPort} region=${evt.neighbor?.regionId}`) }) app.on('reconnect-hint', (hint) => { if (hint && hint.banner) console.log(hint.banner) else if (hint && hint.address) { console.log(`[mesh] RECONNECT Java Edition → ${hint.address}`) } }) app.on('migrate-failed', (evt) => { console.log(`[mesh] migrate failed (${evt.kind || 'error'}) restored=${!!evt.restored}`) }) // Type lines to chat (host/join sessions) if (process.stdin && process.stdin.isTTY && typeof process.stdin.on === 'function') { try { process.stdin.setEncoding('utf8') process.stdin.resume() let buf = '' process.stdin.on('data', (chunk) => { buf += chunk let i while ((i = buf.indexOf('\n')) >= 0) { const line = buf.slice(0, i).replace(/\r$/, '').trim() buf = buf.slice(i + 1) if (!line) continue if (line === '/peers') { const st = app.getStatus() console.log( '[peers]', st.peers.length ? st.peers.map((p) => `${p.name}(${p.id.slice(0, 8)})`).join(', ') : '(none)' ) continue } try { app.sendChat(line) } catch (err) { console.error('[chat]', err.message) } } }) } catch { // non-interactive } } } /** Attach signal handlers once; re-entrant safe via app.exit */ function attachShutdown(app) { let stopping = false const shutdown = async (code) => { if (stopping) return stopping = true console.log('\nStopping…') try { await app.exit(code) } catch (err) { console.error('[shutdown]', err.message || err) } exit(code) } process.on('SIGINT', () => { shutdown(130) }) process.on('SIGTERM', () => { shutdown(143) }) if (isBare && typeof Bare !== 'undefined' && Bare.on) { try { Bare.on('exit', () => { // best-effort; async exit may not finish }) } catch { // ignore } } return shutdown } const createCmd = command( 'create', summary('Create a new local world'), arg('', 'world name / id'), flag('--version [ver]', 'Minecraft version for Flying Squid'), flag('--motd [text]', 'server MOTD'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) await app.ready() try { const meta = app.createWorld({ name: cmd.args.name, version: cmd.flags.version, motd: cmd.flags.motd }) console.log('Created world:', meta.id) console.log(' version:', meta.version) console.log(' path: ', path.join(storage, 'worlds', meta.id)) } finally { await app.close() } } ) const listCmd = command( 'list', summary('List local worlds'), async () => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) await app.ready() try { const worlds = app.listWorlds() if (!worlds.length) { console.log('No worlds yet. Create one: flying-jib create ') return } for (const w of worlds) { console.log(`- ${w.id} (${w.version}) ${w.name}`) } } finally { await app.close() } } ) const startCmd = command( 'start', summary('Start local Flying Squid (127.0.0.1 only, no P2P share)'), arg('', 'world name / id'), flag('--port|-p [port]', 'local Minecraft port (default 25565)'), flag('--version [ver]', 'override Minecraft version'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) app.on('message', (m) => console.log(m)) app.on('error', (err) => console.error('[error]', err)) attachShutdown(app) await app.ready() try { const status = await app.startWorld({ world: cmd.args.name, port: cmd.flags.port ? Number(cmd.flags.port) : 25565, version: cmd.flags.version }) console.log('') console.log(`${appName} local world running`) console.log(` world: ${cmd.args.name}`) console.log(` bind: ${status.host}:${status.port}`) console.log('') console.log('Connect Java Edition to:') console.log(` ${status.host}:${status.port}`) console.log('') console.log('Press Ctrl+C to stop.') await new Promise(() => {}) } catch (err) { console.error('[start failed]', err) await app.close() exit(1) } } ) const hostCmd = command( 'host', summary('Start world and share via HyperDHT (prints fj1. invite)'), arg('', 'world name / id'), flag('--port|-p [port]', 'local Minecraft port (default 25565)'), flag('--version [ver]', 'override Minecraft version'), flag('--name|-n [display]', 'chat display name'), flag('--mc-name [user]', 'Minecraft username for mesh migrate filter'), flag('--mesh', 'create a new mesh and enroll this world'), flag('--mesh-invite [invite]', 'open mesh invite and enroll this world'), flag('--min-x [n]', 'enroll bounds minX'), flag('--max-x [n]', 'enroll bounds maxX'), flag('--min-z [n]', 'enroll bounds minZ'), flag('--max-z [n]', 'enroll bounds maxZ'), flag('--ttl [duration]', 'invite TTL (default 7d; e.g. 24h, 30m)'), flag('--no-expire', 'mint invite with no expiry (discouraged)'), flag('--role [role]', 'invite role hint: player|viewer (default player)'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false, cmd.flags.name, cmd.flags.mcName) app.on('message', (m) => console.log(m)) app.on('error', (err) => console.error('[error]', err)) attachShutdown(app) attachPeerUi(app) await app.ready() try { const enroll = { minX: cmd.flags.minX, maxX: cmd.flags.maxX, minZ: cmd.flags.minZ, maxZ: cmd.flags.maxZ } const result = await app.hostWorld({ world: cmd.args.name, port: cmd.flags.port ? Number(cmd.flags.port) : 25565, version: cmd.flags.version, displayName: cmd.flags.name, mcUsername: cmd.flags.mcName, createMesh: !!cmd.flags.mesh, meshInvite: cmd.flags.meshInvite, enroll, ttl: cmd.flags.ttl, noExpire: !!cmd.flags.noExpire, role: cmd.flags.role || 'player' }) console.log('') console.log(`${appName} hosting private world (P2P)`) console.log(` world: ${cmd.args.name}`) console.log(` local: ${result.squid.host}:${result.squid.port}`) console.log(` tunnel: HyperDHT host`) console.log(` chat: type lines + Enter; /peers for list`) if (result.region) { console.log(` mesh: enrolled ${result.region.regionId}`) console.log(` border: active (kick+migrate signal on leave)`) } if (result.inviteMeta) { console.log( ` invite: role=${result.inviteMeta.role || 'player'}` + (result.inviteMeta.expires ? ` expires=${new Date(result.inviteMeta.expires).toISOString()}` : ' expires=never') ) } console.log('') console.log('Invite (treat as secret — fj1. capability; share out-of-band only):') console.log(result.invite) if (result.mesh && result.mesh.invite) { console.log('') console.log('Mesh invite (also secret):') console.log(result.mesh.invite) } console.log('') console.log('Friends run:') console.log(` flying-jib join '' --mc-name `) console.log('') console.log('You connect Java Edition to:') console.log(` ${result.squid.host}:${result.squid.port}`) console.log('') console.log('To invalidate outstanding invites later: flying-jib rotate-cap ') console.log('Press Ctrl+C to stop.') await new Promise(() => {}) } catch (err) { console.error('[host failed]', err) await app.close() exit(1) } } ) const joinCmd = command( 'join', summary('Join a private world from an fj1. invite'), arg('', 'fj1. invite string'), flag('--port|-p [port]', 'local bind port (default: ephemeral)'), flag('--name|-n [display]', 'chat display name'), flag('--mc-name [user]', 'Minecraft username (mesh migrate filter)'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false, cmd.flags.name, cmd.flags.mcName) app.on('message', (m) => console.log(m)) app.on('error', (err) => console.error('[error]', err)) attachShutdown(app) attachPeerUi(app) await app.ready() try { const result = await app.joinWorld({ invite: cmd.args.invite, localPort: cmd.flags.port ? Number(cmd.flags.port) : 0, displayName: cmd.flags.name, mcUsername: cmd.flags.mcName }) const port = result.tunnel.localPort console.log('') console.log(`${appName} joined remote world`) console.log(` name: ${result.invite.name || '(unnamed)'}`) console.log(` version: ${result.invite.mcVersion || 'unknown'}`) console.log(` role: ${result.invite.role || 'player'}`) if (result.invite.expires) { console.log(` expires: ${new Date(result.invite.expires).toISOString()}`) } console.log(` local: 127.0.0.1:${port}`) console.log(` chat: type lines + Enter; /peers for list`) console.log(` migrate: auto if host signals mesh border for --mc-name`) console.log('') console.log('Connect Java Edition to:') console.log(` 127.0.0.1:${port}`) console.log('') console.log('Press Ctrl+C to stop.') await new Promise(() => {}) } catch (err) { console.error('[join failed]', err) await app.close() exit(1) } } ) const statusCmd = command( 'status', summary('Show storage path and worlds'), async () => { const storage = resolveStorage(root.flags.storage) console.log('storage:', storage) const app = makeApp(storage, false) await app.ready() try { const worlds = app.listWorlds() console.log('worlds:', worlds.length) for (const w of worlds) console.log(' -', w.id, w.version) const meshes = app.listMeshes() console.log('meshes:', meshes.length) for (const m of meshes) console.log(' -', m.meshId, m.key) } finally { await app.close() } } ) const meshCreateCmd = command( 'mesh-create', summary('Create a new mesh and print fj1. mesh invite'), flag('--name [name]', 'mesh display name'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) await app.ready() try { const r = await app.createMesh({ name: cmd.flags.name }) console.log('Mesh created') console.log(' meshId:', r.meshId) console.log(' key: ', r.key) console.log(' writer:', r.localWriterKey) console.log('') console.log('Mesh invite (share to join registry):') console.log(r.invite) console.log('') console.log('Keep process alive to seed registry, or re-open later with mesh-open.') console.log('Enroll a world: flying-jib mesh-enroll --min-x 0 --max-x 9999 ...') // Keep mesh swarm alive until Ctrl+C attachShutdown(app) await new Promise(() => {}) } catch (err) { console.error('[mesh-create failed]', err) await app.close() exit(1) } } ) const meshOpenCmd = command( 'mesh-open', summary('Open mesh by key or fj1. mesh invite'), flag('--key [key]', 'mesh public key (z32)'), flag('--invite [invite]', 'fj1. mesh invite'), flag('--name [name]', 'local display name for mesh folder'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) await app.ready() try { const r = await app.openMesh({ key: cmd.flags.key, invite: cmd.flags.invite, name: cmd.flags.name }) console.log('Mesh open') console.log(' meshId: ', r.meshId) console.log(' key: ', r.key) console.log(' writable:', r.writable) console.log(' writer: ', r.localWriterKey) if (!r.writable) { console.log('') console.log('Not yet a writer. Ask a mesh writer to run:') console.log(` flying-jib mesh-admit ${r.localWriterKey}`) } attachShutdown(app) await new Promise(() => {}) } catch (err) { console.error('[mesh-open failed]', err) await app.close() exit(1) } } ) const meshAdmitCmd = command( 'mesh-admit', summary('Admit a peer writer key (requires open writable mesh in this process)'), arg('', 'peer local writer key (z32 or hex)'), flag('--invite [invite]', 'open mesh from invite first'), flag('--key [key]', 'open mesh from key first'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) await app.ready() try { if (cmd.flags.invite || cmd.flags.key) { await app.openMesh({ key: cmd.flags.key, invite: cmd.flags.invite }) } if (!app.mesh) throw new Error('No mesh open; pass --invite or --key') await app.meshAddWriter(cmd.args.writerKey) console.log('Writer admitted:', cmd.args.writerKey) await app.stopMesh() await app.close() } catch (err) { console.error('[mesh-admit failed]', err) await app.close() exit(1) } } ) const meshEnrollCmd = command( 'mesh-enroll', summary('Enroll a local world region into the open mesh'), arg('', 'world id to enroll'), flag('--invite [invite]', 'mesh invite to open'), flag('--key [key]', 'mesh key to open'), flag('--min-x [n]', 'bounds minX (default 0)'), flag('--max-x [n]', 'bounds maxX (default 9999)'), flag('--min-z [n]', 'bounds minZ (default 0)'), flag('--max-z [n]', 'bounds maxZ (default 9999)'), flag('--offset-x [n]', 'region offset x'), flag('--offset-z [n]', 'region offset z'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) await app.ready() try { if (cmd.flags.invite || cmd.flags.key) { await app.openMesh({ key: cmd.flags.key, invite: cmd.flags.invite }) } if (!app.mesh) throw new Error('No mesh open; pass --invite or --key') const region = await app.enrollWorld({ world: cmd.args.world, minX: cmd.flags.minX, maxX: cmd.flags.maxX, minZ: cmd.flags.minZ, maxZ: cmd.flags.maxZ, offsetX: cmd.flags.offsetX, offsetZ: cmd.flags.offsetZ }) console.log('Enrolled region:') console.log(JSON.stringify(region, null, 2)) await app.stopMesh() await app.close() } catch (err) { console.error('[mesh-enroll failed]', err) await app.close() exit(1) } } ) const meshListCmd = command( 'mesh-list', summary('List regions in a mesh'), flag('--invite [invite]', 'mesh invite'), flag('--key [key]', 'mesh key'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) await app.ready() try { if (cmd.flags.invite || cmd.flags.key) { await app.openMesh({ key: cmd.flags.key, invite: cmd.flags.invite }) } if (!app.mesh) throw new Error('No mesh open; pass --invite or --key') const regions = await app.listMeshRegions() if (!regions.length) { console.log('(no regions enrolled yet)') } else { for (const r of regions) { console.log( `- ${r.regionId} ${r.name || ''} bounds=${JSON.stringify(r.bounds)} worldKey=${r.worldKey.slice(0, 12)}…` ) } } await app.stopMesh() await app.close() } catch (err) { console.error('[mesh-list failed]', err) await app.close() exit(1) } } ) const inviteCmd = command( 'invite', summary('Mint a private-world fj1. invite (default TTL 7d) without starting Squid'), arg('', 'world name / id'), flag('--ttl [duration]', 'invite TTL (default 7d; e.g. 24h, 30m)'), flag('--no-expire', 'no expiry (discouraged)'), flag('--role [role]', 'player|viewer (default player)'), flag('--port-hint [port]', 'portHint field (default 25565)'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) app.on('message', (m) => console.log(m)) await app.ready() try { const result = app.mintInvite({ world: cmd.args.name, ttl: cmd.flags.ttl, noExpire: !!cmd.flags.noExpire, role: cmd.flags.role || 'player', portHint: cmd.flags.portHint ? Number(cmd.flags.portHint) : 25565 }) console.log('') console.log('Invite (secret — share carefully):') console.log(result.invite) console.log('') console.log( `role=${result.inviteMeta.role || 'player'}` + (result.inviteMeta.expires ? ` expires=${new Date(result.inviteMeta.expires).toISOString()}` : ' expires=never') ) console.log('Invalidate all invites for this world: flying-jib rotate-cap ' + cmd.args.name) await app.close() } catch (err) { console.error('[invite failed]', err) await app.close() exit(1) } } ) const rotateCapCmd = command( 'rotate-cap', summary('Rotate tunnel capability — invalidates all outstanding private invites'), arg('', 'world name / id'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) app.on('message', (m) => console.log(m)) await app.ready() try { const r = await app.rotateWorldCap({ world: cmd.args.name }) console.log('') console.log('Capability rotated. Previous invites no longer work.') console.log(` worldKey unchanged (DHT identity kept)`) console.log(` revoked fingerprint: ${r.revokedFp.slice(0, 16)}…`) console.log('') console.log('Mint a new invite:') console.log(` flying-jib invite ${cmd.args.name}`) await app.close() } catch (err) { console.error('[rotate-cap failed]', err) await app.close() exit(1) } } ) const revokeCmd = command( 'revoke', summary('Record a cap/invite fingerprint on the local revoke ledger (audit)'), arg('', 'world name / id'), arg('', 'z32 cap or full fj1. invite'), flag('--reason [text]', 'reason tag'), async (cmd) => { const storage = resolveStorage(root.flags.storage) const app = makeApp(storage, false) app.on('message', (m) => console.log(m)) await app.ready() try { const r = app.revokeWorldCap({ world: cmd.args.name, capOrInvite: cmd.args.capOrInvite, reason: cmd.flags.reason }) console.log(r.already ? 'Already on revoke ledger.' : 'Recorded on revoke ledger.') console.log(` fingerprint: ${r.fp}`) console.log('Note: live access is invalidated by rotate-cap (new active cap).') await app.close() } catch (err) { console.error('[revoke failed]', err) await app.close() exit(1) } } ) const root = command( appName, header(`${appName} — decentralized P2P Minecraft (Bare/Pear)`), summary(pkg.description), flag('--version|-v', 'Print version'), flag('--storage ', 'App storage directory'), footer('Docs: living_docs/ | agent/ADRs/0013-bare-pear-only-runtime-and-distribution.md'), createCmd, listCmd, startCmd, hostCmd, joinCmd, inviteCmd, rotateCapCmd, revokeCmd, statusCmd, meshCreateCmd, meshOpenCmd, meshAdmitCmd, meshEnrollCmd, meshListCmd ) root.parse(rawArgv) if (root.flags.version) { console.log(`${appName} v${pkg.version}`) exit(0) }