|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +/** |
| 4 | + * PhonePod CLI |
| 5 | + * |
| 6 | + * Solid pod on your phone - Android/Termux |
| 7 | + * |
| 8 | + * Usage: |
| 9 | + * phonepod start [options] Start the pod |
| 10 | + * phonepod setup Interactive setup |
| 11 | + * phonepod tunnel <host> Setup SSH tunnel to relay |
| 12 | + * phonepod status Show status |
| 13 | + */ |
| 14 | + |
| 15 | +import { spawn } from 'child_process'; |
| 16 | +import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; |
| 17 | +import { join } from 'path'; |
| 18 | +import { homedir } from 'os'; |
| 19 | +import { createInterface } from 'readline'; |
| 20 | + |
| 21 | +const args = process.argv.slice(2); |
| 22 | +const command = args[0]; |
| 23 | + |
| 24 | +// Config directory |
| 25 | +const CONFIG_DIR = join(homedir(), '.phonepod'); |
| 26 | +const CONFIG_FILE = join(CONFIG_DIR, 'config.json'); |
| 27 | + |
| 28 | +// Ensure config dir exists |
| 29 | +if (!existsSync(CONFIG_DIR)) { |
| 30 | + mkdirSync(CONFIG_DIR, { recursive: true }); |
| 31 | +} |
| 32 | + |
| 33 | +// Load or create config |
| 34 | +function loadConfig() { |
| 35 | + if (existsSync(CONFIG_FILE)) { |
| 36 | + return JSON.parse(readFileSync(CONFIG_FILE, 'utf8')); |
| 37 | + } |
| 38 | + return { port: 8080, nostr: true, git: true }; |
| 39 | +} |
| 40 | + |
| 41 | +function saveConfig(config) { |
| 42 | + writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); |
| 43 | +} |
| 44 | + |
| 45 | +// Run JSS with phonepod defaults |
| 46 | +function startPod(options = {}) { |
| 47 | + const config = loadConfig(); |
| 48 | + const jssArgs = [ |
| 49 | + 'start', |
| 50 | + '--port', String(options.port || config.port || 8080), |
| 51 | + ]; |
| 52 | + |
| 53 | + if (options.nostr ?? config.nostr) jssArgs.push('--nostr'); |
| 54 | + if (options.git ?? config.git) jssArgs.push('--git'); |
| 55 | + if (options.root) jssArgs.push('--root', options.root); |
| 56 | + |
| 57 | + console.log('Starting PhonePod...'); |
| 58 | + console.log(` Port: ${options.port || config.port || 8080}`); |
| 59 | + console.log(` Nostr: ${(options.nostr ?? config.nostr) ? 'enabled' : 'disabled'}`); |
| 60 | + console.log(` Git: ${(options.git ?? config.git) ? 'enabled' : 'disabled'}`); |
| 61 | + console.log(''); |
| 62 | + |
| 63 | + // Find jss binary |
| 64 | + const jss = spawn('jss', jssArgs, { stdio: 'inherit' }); |
| 65 | + |
| 66 | + jss.on('error', (err) => { |
| 67 | + if (err.code === 'ENOENT') { |
| 68 | + console.error('Error: jss not found. Run: npm install -g javascript-solid-server'); |
| 69 | + } else { |
| 70 | + console.error('Error:', err.message); |
| 71 | + } |
| 72 | + process.exit(1); |
| 73 | + }); |
| 74 | + |
| 75 | + jss.on('exit', (code) => process.exit(code || 0)); |
| 76 | +} |
| 77 | + |
| 78 | +// Setup tunnel |
| 79 | +async function setupTunnel(relayHost) { |
| 80 | + if (!relayHost) { |
| 81 | + console.error('Usage: phonepod tunnel <relay-host>'); |
| 82 | + console.error('Example: phonepod tunnel relay.example.com'); |
| 83 | + process.exit(1); |
| 84 | + } |
| 85 | + |
| 86 | + const config = loadConfig(); |
| 87 | + const port = config.port || 8080; |
| 88 | + |
| 89 | + console.log(''); |
| 90 | + console.log('Setting up SSH tunnel to', relayHost); |
| 91 | + console.log(''); |
| 92 | + |
| 93 | + // Check for SSH key |
| 94 | + const sshKeyPath = join(homedir(), '.ssh', 'id_ed25519'); |
| 95 | + const sshPubKeyPath = sshKeyPath + '.pub'; |
| 96 | + |
| 97 | + if (!existsSync(sshKeyPath)) { |
| 98 | + console.log('Generating SSH key...'); |
| 99 | + const keygen = spawn('ssh-keygen', ['-t', 'ed25519', '-f', sshKeyPath, '-N', ''], { stdio: 'inherit' }); |
| 100 | + await new Promise(resolve => keygen.on('exit', resolve)); |
| 101 | + } |
| 102 | + |
| 103 | + // Show public key |
| 104 | + if (existsSync(sshPubKeyPath)) { |
| 105 | + const pubKey = readFileSync(sshPubKeyPath, 'utf8').trim(); |
| 106 | + console.log(''); |
| 107 | + console.log('Add this SSH key to your relay server:'); |
| 108 | + console.log(''); |
| 109 | + console.log(pubKey); |
| 110 | + console.log(''); |
| 111 | + } |
| 112 | + |
| 113 | + // Create tunnel script |
| 114 | + const tunnelScript = join(CONFIG_DIR, 'tunnel.sh'); |
| 115 | + writeFileSync(tunnelScript, `#!/bin/bash |
| 116 | +exec autossh -M 0 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 -N -R ${port}:localhost:${port} ${relayHost} |
| 117 | +`); |
| 118 | + spawn('chmod', ['+x', tunnelScript]); |
| 119 | + |
| 120 | + console.log(`Tunnel script created: ${tunnelScript}`); |
| 121 | + console.log(''); |
| 122 | + console.log('To start tunnel manually:'); |
| 123 | + console.log(` ${tunnelScript}`); |
| 124 | + console.log(''); |
| 125 | + console.log('To add to PM2:'); |
| 126 | + console.log(` pm2 start ${tunnelScript} --name tunnel`); |
| 127 | + console.log(' pm2 save'); |
| 128 | + console.log(''); |
| 129 | + |
| 130 | + // Save relay host to config |
| 131 | + config.relayHost = relayHost; |
| 132 | + saveConfig(config); |
| 133 | +} |
| 134 | + |
| 135 | +// Show status |
| 136 | +function showStatus() { |
| 137 | + const config = loadConfig(); |
| 138 | + console.log(''); |
| 139 | + console.log('PhonePod Configuration:'); |
| 140 | + console.log(''); |
| 141 | + console.log(` Port: ${config.port || 8080}`); |
| 142 | + console.log(` Nostr: ${config.nostr ? 'enabled' : 'disabled'}`); |
| 143 | + console.log(` Git: ${config.git ? 'enabled' : 'disabled'}`); |
| 144 | + if (config.relayHost) { |
| 145 | + console.log(` Relay: ${config.relayHost}`); |
| 146 | + } |
| 147 | + console.log(''); |
| 148 | + console.log(` Config: ${CONFIG_FILE}`); |
| 149 | + console.log(''); |
| 150 | +} |
| 151 | + |
| 152 | +// Help |
| 153 | +function showHelp() { |
| 154 | + console.log(` |
| 155 | +PhonePod - Solid pod on your phone |
| 156 | +
|
| 157 | +Usage: |
| 158 | + phonepod start [options] Start the pod |
| 159 | + phonepod tunnel <host> Setup SSH tunnel to relay |
| 160 | + phonepod status Show configuration |
| 161 | + phonepod help Show this help |
| 162 | +
|
| 163 | +Start Options: |
| 164 | + --port <n> Port to listen on (default: 8080) |
| 165 | + --no-nostr Disable Nostr relay |
| 166 | + --no-git Disable Git server |
| 167 | +
|
| 168 | +Examples: |
| 169 | + phonepod start |
| 170 | + phonepod start --port 3000 |
| 171 | + phonepod tunnel ubuntu@relay.example.com |
| 172 | +`); |
| 173 | +} |
| 174 | + |
| 175 | +// Parse start options |
| 176 | +function parseStartOptions() { |
| 177 | + const options = {}; |
| 178 | + for (let i = 1; i < args.length; i++) { |
| 179 | + if (args[i] === '--port' && args[i + 1]) { |
| 180 | + options.port = parseInt(args[++i], 10); |
| 181 | + } else if (args[i] === '--no-nostr') { |
| 182 | + options.nostr = false; |
| 183 | + } else if (args[i] === '--no-git') { |
| 184 | + options.git = false; |
| 185 | + } else if (args[i] === '--root' && args[i + 1]) { |
| 186 | + options.root = args[++i]; |
| 187 | + } |
| 188 | + } |
| 189 | + return options; |
| 190 | +} |
| 191 | + |
| 192 | +// Main |
| 193 | +switch (command) { |
| 194 | + case 'start': |
| 195 | + startPod(parseStartOptions()); |
| 196 | + break; |
| 197 | + case 'tunnel': |
| 198 | + setupTunnel(args[1]); |
| 199 | + break; |
| 200 | + case 'status': |
| 201 | + showStatus(); |
| 202 | + break; |
| 203 | + case 'help': |
| 204 | + case '--help': |
| 205 | + case '-h': |
| 206 | + showHelp(); |
| 207 | + break; |
| 208 | + default: |
| 209 | + if (command) { |
| 210 | + console.error(`Unknown command: ${command}`); |
| 211 | + } |
| 212 | + showHelp(); |
| 213 | + process.exit(command ? 1 : 0); |
| 214 | +} |
0 commit comments