303 lines
8.6 KiB
JavaScript
303 lines
8.6 KiB
JavaScript
/**
|
|
* Flying Jib CLI — Bare entry (ADR-0013).
|
|
*
|
|
* Usage:
|
|
* bare bin.mjs create <name>
|
|
* bare bin.mjs list
|
|
* bare bin.mjs start <name> [--port 25565]
|
|
* bare bin.mjs host <name> [--port 25565] # start + share invite
|
|
* bare bin.mjs join <invite> [--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('<name>', '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 <name>')
|
|
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('<name>', '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('<name>', '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 '<invite>'`)
|
|
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('<invite>', '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 <dir>', '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
|