/** * 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 { fileURLToPath } from 'url' import pkg from './package.json' with { type: 'json' } import App from './app.js' const require = createRequire(import.meta.url) const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) 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 resolveStorage(flagStorage) { if (flagStorage) return path.resolve(flagStorage) const os = require('os') if (isBare) { try { const { persistent } = require('bare-storage') return isDev ? path.join(os.tmpdir(), 'pear', appName) : path.join(persistent(), appName) } catch { return path.join(os.tmpdir(), 'pear', appName) } } return path.join(os.tmpdir(), 'pear', appName) } function exit(code) { if (isBare) Bare.exit(code) else process.exit(code) } function makeApp(storage, updates = false) { return new App({ dir: storage, appPath: isDev ? null : argv0, updates, version: pkg.version, upgrade: pkg.upgrade, name: appName }) } /** 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'), 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 result = await app.hostWorld({ world: cmd.args.name, port: cmd.flags.port ? Number(cmd.flags.port) : 25565, version: cmd.flags.version }) 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('') console.log('Invite (treat as secret — fj1. capability):') console.log(result.invite) console.log('') console.log('Friends run:') console.log(` flying-jib join ''`) console.log('') console.log('You connect Java Edition to:') console.log(` ${result.squid.host}:${result.squid.port}`) console.log('') 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)'), 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 result = await app.joinWorld({ invite: cmd.args.invite, localPort: cmd.flags.port ? Number(cmd.flags.port) : 0 }) 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(` local: 127.0.0.1:${port}`) 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) } finally { await app.close() } } ) 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, statusCmd ) root.parse(rawArgv) if (root.flags.version) { console.log(`${appName} v${pkg.version}`) exit(0) } void __dirname void __filename