#!/usr/bin/env node
import { Registry } from '../lib/registry.js'
import { Deployer } from '../lib/deployer.js'
import { generateConfig } from '../lib/config-generator.js'
const args = process.argv.slice(2)
const command = args[0]
const subcommand = args[1]
function parseFlags (args) {
const flags = {}
for (let i = 0; i < args.length; i++) {
if (args[i].startsWith('--')) {
const key = args[i].slice(2)
const val = args[i + 1] && !args[i + 1].startsWith('--') ? args[i + 1] : true
flags[key] = val
if (val !== true) i++
}
}
return flags
}
function usage () {
console.log(`Usage: 7rnx <command> [options]
Node Management:
7rnx node add <petname> --ip <vpn-ip> [--keet-ident <key>]
7rnx node list
7rnx node remove <petname>
Service Management:
7rnx service add <node> <name> --port <port> [--prefix <path>] [--root-path <path>]
7rnx service add-backend <service> --node <petname> --port <port> [--mode active|standby]
7rnx service remove-backend <service> --node <petname>
7rnx service list [node]
7rnx service remove <node> <name>
7rnx service status [node] Check which services are actually listening
Host (vhost) Management — subdomain reverse proxy at the URL root (for atproto PDS etc.):
7rnx host add <name> <node> --port <port> --server-name <fqdn>
7rnx host list
7rnx host remove <name>
Deployment:
7rnx deploy Deploy path-config + vhosts to 7rnx.net and reload Apache
7rnx deploy --dry-run Show generated config (path + vhosts) without deploying
7rnx status Check Apache status on 7rnx.net
Remote:
7rnx remote config [path] Fetch live config from 7rnx.net (default: SSL vhost)
7rnx remote ls List sites-enabled configs
Web UI:
7rnx ui [port] Start dashboard (default: 7777)
7rnx ui --network Bind to 0.0.0.0 (LAN access)
Config:
7rnx config Show current registry
7rnx config set-server --host <ip> --user <user>`)
}
function main () {
if (!command || command === 'help' || command === '--help') {
usage()
process.exit(0)
}
const registry = new Registry().load()
switch (command) {
case 'node':
handleNode(registry, subcommand, args.slice(2))
break
case 'service':
handleService(registry, subcommand, args.slice(2))
break
case 'host':
handleHost(registry, subcommand, args.slice(2))
break
case 'deploy':
handleDeploy(registry, args.slice(1))
break
case 'status':
handleStatus(registry)
break
case 'remote':
handleRemote(registry, subcommand, args.slice(2))
break
case 'ui':
handleUi(args.slice(1))
break
case 'config':
handleConfig(registry, subcommand, args.slice(2))
break
default:
console.error(`Unknown command: ${command}`)
usage()
process.exit(1)
}
}
function handleNode (registry, sub, rest) {
switch (sub) {
case 'add': {
const petname = rest[0]
const flags = parseFlags(rest.slice(1))
if (!petname || !flags.ip) {
console.error('Usage: 7rnx node add <petname> --ip <vpn-ip> [--keet-ident <key>]')
process.exit(1)
}
registry.addNode(petname, flags.ip, flags['keet-ident'])
console.log(`Added node "${petname}" (${flags.ip})`)
break
}
case 'list': {
const nodes = registry.listNodes()
if (nodes.length === 0) {
console.log('No nodes registered.')
return
}
for (const n of nodes) {
const ident = n.keetIdent ? ` [${n.keetIdent.slice(0, 8)}...]` : ''
console.log(` ${n.petname}\t${n.ip}${ident}`)
}
break
}
case 'remove': {
const petname = rest[0]
if (!petname) {
console.error('Usage: 7rnx node remove <petname>')
process.exit(1)
}
const services = registry.listServices(petname)
registry.removeNode(petname)
console.log(`Removed node "${petname}" and ${services.length} service(s)`)
break
}
default:
console.error('Usage: 7rnx node <add|list|remove>')
process.exit(1)
}
}
function handleService (registry, sub, rest) {
switch (sub) {
case 'add': {
const nodePetname = rest[0]
const serviceName = rest[1]
const flags = parseFlags(rest.slice(2))
if (!nodePetname || !serviceName || !flags.port) {
console.error('Usage: 7rnx service add <node> <name> --port <port> [--prefix <path>] [--root-path <path>]')
process.exit(1)
}
registry.addService(nodePetname, serviceName, flags.port, flags.prefix, flags['root-path'])
const urlPath = flags['root-path'] || `/${nodePetname}/${serviceName}/`
console.log(`Added service "${serviceName}" on "${nodePetname}" → ${urlPath} → :${flags.port}`)
break
}
case 'list': {
const nodePetname = rest[0]
const services = registry.listServices(nodePetname)
if (services.length === 0) {
console.log(nodePetname ? `No services on node "${nodePetname}".` : 'No services registered.')
return
}
for (const s of services) {
const urlPath = s.rootPath || `/${s.node}/${s.name}/`
if (s.backends && s.backends.length > 1) {
console.log(` ${s.name}\t→ ${urlPath} (balancer)`)
for (const b of s.backends) {
console.log(` ${b.node}:${b.port}\t${b.mode}`)
}
} else {
console.log(` ${s.node}/${s.name}\t:${s.port}\t→ ${urlPath}`)
}
}
break
}
case 'remove': {
const nodePetname = rest[0]
const serviceName = rest[1]
if (!nodePetname || !serviceName) {
console.error('Usage: 7rnx service remove <node> <name>')
process.exit(1)
}
registry.removeService(nodePetname, serviceName)
console.log(`Removed service "${serviceName}" from "${nodePetname}"`)
break
}
case 'add-backend': {
const serviceName = rest[0]
const flags = parseFlags(rest.slice(1))
if (!serviceName || !flags.node || !flags.port) {
console.error('Usage: 7rnx service add-backend <service> --node <petname> --port <port> [--mode active|standby]')
process.exit(1)
}
registry.addBackend(serviceName, flags.node, flags.port, flags.mode || 'standby')
console.log(`Added backend: ${flags.node}:${flags.port} (${flags.mode || 'standby'}) to service "${serviceName}"`)
break
}
case 'remove-backend': {
const serviceName = rest[0]
const flags = parseFlags(rest.slice(1))
if (!serviceName || !flags.node) {
console.error('Usage: 7rnx service remove-backend <service> --node <petname>')
process.exit(1)
}
registry.removeBackend(serviceName, flags.node)
console.log(`Removed backend "${flags.node}" from service "${serviceName}"`)
break
}
case 'status': {
const nodePetname = rest[0]
const deployer = new Deployer(registry.data)
const services = registry.listServices(nodePetname)
if (services.length === 0) {
console.log(nodePetname ? `No services on node "${nodePetname}".` : 'No services registered.')
return
}
const results = deployer.checkServices(registry.data.nodes, services)
for (const r of results) {
const icon = r.status === 'UP' ? '\x1b[32mUP\x1b[0m' : '\x1b[31m' + r.status + '\x1b[0m'
const urlPath = services.find(s => s.node === r.node && s.name === r.name)
const url = urlPath?.rootPath || `/${r.node}/${r.name}/`
console.log(` ${icon}\t${r.node}/${r.name}\t:${r.port}\t${url}`)
}
break
}
default:
console.error('Usage: 7rnx service <add|list|remove|add-backend|remove-backend|status>')
process.exit(1)
}
}
function handleHost (registry, sub, rest) {
switch (sub) {
case 'add': {
const name = rest[0]
const nodePetname = rest[1]
const flags = parseFlags(rest.slice(2))
if (!name || !nodePetname || !flags.port || !flags['server-name']) {
console.error('Usage: 7rnx host add <name> <node> --port <port> --server-name <fqdn>')
process.exit(1)
}
registry.addHost(name, nodePetname, flags.port, flags['server-name'])
console.log(`Added host "${name}": https://${flags['server-name']}/ → ${nodePetname}:${flags.port}`)
console.log('Run `7rnx deploy` to apply (requires the vhosts sudoers rule from setup-7rnx-server.sh).')
break
}
case 'list': {
const hosts = registry.listHosts()
if (hosts.length === 0) { console.log('No vhosts registered.'); return }
for (const h of hosts) console.log(` ${h.name}\t${h.serverName}\t→ ${h.node}:${h.port}`)
break
}
case 'remove': {
const name = rest[0]
if (!name) { console.error('Usage: 7rnx host remove <name>'); process.exit(1) }
registry.removeHost(name)
console.log(`Removed host "${name}"`)
break
}
default:
console.error('Usage: 7rnx host <add|list|remove>')
process.exit(1)
}
}
function handleDeploy (registry, rest) {
const flags = parseFlags(rest)
const deployer = new Deployer(registry.data)
const hasHosts = (registry.data.hosts || []).length > 0
if (flags['dry-run']) {
console.log('# ===== path-config (/etc/apache2/7rnx-proxy.conf) =====')
console.log(deployer.dryRun())
if (hasHosts) {
console.log('\n# ===== vhosts (/etc/apache2/sites-enabled/7rnx-proxy-vhosts.conf) =====')
console.log(deployer.dryRunVhosts())
}
return
}
deployer.deploy()
if (hasHosts) deployer.deployVhosts()
}
function handleStatus (registry) {
const deployer = new Deployer(registry.data)
deployer.status()
}
function handleRemote (registry, sub, rest) {
const deployer = new Deployer(registry.data)
switch (sub) {
case 'config': {
const configPath = rest[0] || '/etc/apache2/sites-enabled/000-default-le-ssl.conf'
console.log(`# Fetching ${configPath} from ${registry.data.server.host}\n`)
const content = deployer.fetchRemoteConfig(configPath)
if (content) console.log(content)
break
}
case 'ls': {
const listing = deployer.listRemoteConfigs()
if (listing) console.log(listing.trim())
break
}
default:
console.error('Usage: 7rnx remote <config|ls>')
process.exit(1)
}
}
async function handleUi (rest) {
const flags = parseFlags(rest)
const port = rest.find(a => !a.startsWith('--')) || 7777
const { startServer } = await import('../lib/web.js')
startServer(Number(port), !!flags.network)
}
function handleConfig (registry, sub, rest) {
if (sub === 'set-server') {
const flags = parseFlags(rest)
if (!flags.host && !flags.user) {
console.error('Usage: 7rnx config set-server --host <ip> --user <user>')
process.exit(1)
}
registry.setServer(flags.host, flags.user)
console.log(`Server: ${registry.data.server.user}@${registry.data.server.host}`)
return
}
// Default: show registry
console.log(JSON.stringify(registry.data, null, 2))
}
main()