Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
6.3 kB · 198 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199#!/usr/bin/env bun/** * Sync-server lifecycle CLI. * * bun run test:pushwork-sync:server:up Start the sync server (idempotent). * bun run test:pushwork-sync:server:down Stop it. * bun run test:pushwork-sync:server:status Show running state. * bun run test:pushwork-sync:server:logs Tail the server log. * bun run test:pushwork-sync:server:wipe Stop + delete all persistent storage. */
import { spawn } from 'node:child_process'import { existsSync, readdirSync, readFileSync, rmSync } from 'node:fs'
import { getHarnessConfig, type HarnessConfig, syncServerLogFile,} from '../support/env.ts'import { getServerStatus, startSyncServer, stopSyncServer,} from '../support/server.ts'
const HELP = `\Usage: test:pushwork-sync:server <command>
Commands: up Start the sync server (idempotent). down Stop it. status Show running state (PID, URL, storage ID, uptime). logs Print the server log (use 'logs -f' to follow). wipe Stop the server and delete all persistent storage.
Environment: SYNC_SERVER_URL Full ws:// URL (e.g. ws://localhost:3030). If set, derives SYNC_SERVER_PORT from it. SYNC_SERVER_PORT Port to bind. Ignored if SYNC_SERVER_URL is set unless they agree. REAMS_TEST_SYNC_PORT Worktree-local default port (set by .envrc; defaults to 3030 outside direnv). SYNC_SERVER_BIN Override sync-server binary path. PUSHWORK_SYNC_STATE_ROOT Override state root (default .dev-state/pushwork-sync). SYNC_SERVER_STARTUP_TIMEOUT_MS How long to wait for the storage-id line (default 10000).`
async function main() { const [command, ...args] = process.argv.slice(2) const config = getHarnessConfig()
switch (command) { case 'up': { const result = await startSyncServer(config) if (result.state === 'already-running') { process.stdout.write(`Sync server already running (pid ${result.pid}).\n`) } else { process.stdout.write(`Sync server started.\n`) } process.stdout.write( ` URL: ${config.syncServerUrl}\n` + ` PID: ${result.pid}\n` + ` Storage ID: ${result.storageId}\n`, ) break }
case 'down': { const pid = await stopSyncServer(config) if (pid === null) { process.stdout.write('Sync server is not running.\n') } else { process.stdout.write(`Sync server stopped (pid ${pid}).\n`) } break }
case 'status': { const s = getServerStatus(config) if (!s.running) { process.stdout.write('Sync server: stopped.\n') process.stdout.write(` Configured URL: ${s.url}\n`) process.stdout.write(` Data dir: ${s.dataDir}\n`) } else { process.stdout.write( `Sync server: running\n` + ` PID: ${s.pid}\n` + ` URL: ${s.url}\n` + ` Storage ID: ${s.storageId ?? '(unknown)'}\n` + ` Uptime: ${s.uptime ?? '(unknown)'}\n` + ` Data dir: ${s.dataDir}\n` + ` Log file: ${s.logFile}\n`, ) } break }
case 'logs': await runLogs(config, args) break
case 'wipe': await runWipe(config, args) break
case undefined: case '-h': case '--help': process.stdout.write(HELP) break
default: process.stderr.write(`Unknown command: ${command}\n\n${HELP}`) process.exit(2) }}
async function runLogs(config: HarnessConfig, args: string[]) { const path = syncServerLogFile(config) if (!existsSync(path)) { process.stderr.write(`No log file at ${path}. Is the server running?\n`) process.exit(1) } const follow = args.includes('-f') || args.includes('--follow') if (!follow) { process.stdout.write(readFileSync(path, 'utf8')) return } // Follow mode: shell to `tail -f`. The nix shell ships coreutils, so // `tail` is reliably available; non-Linux portability isn't a goal. const tail = spawn('tail', ['-f', path], { stdio: 'inherit' }) process.on('SIGINT', () => { tail.kill('SIGINT') }) await new Promise<void>((resolve) => tail.once('exit', () => resolve()))}
async function runWipe(config: HarnessConfig, args: string[]) { if (!existsSync(config.stateRoot)) { process.stdout.write('Nothing to wipe.\n') return }
// Inventory what will be destroyed so the prompt isn't a black box. const workspacesDir = `${config.stateRoot}/workspaces` const workspaces = existsSync(workspacesDir) ? readdirSync(workspacesDir, { withFileTypes: true }) .filter((d) => d.isDirectory()) .map((d) => d.name) : []
const skipConfirm = args.includes('--yes') || args.includes('-y') if (!skipConfirm) { process.stdout.write( `About to delete:\n` + ` Sync server data: ${config.stateRoot}/data/\n` + ` Server log/state: ${config.stateRoot}/server.*\n` + ` Fixture workspaces: ${workspaces.length === 0 ? '(none)' : workspaces.map((w) => `\n - ${w}`).join('')}\n` + `\n` + `Any in-progress 'pushwork sync' for these workspaces will fail.\n` + `Re-run with --yes to skip this prompt.\n` + `\n` + `Continue? [y/N]: `, ) const answer = await readLineFromStdin() if (!/^y(es)?$/i.test(answer.trim())) { process.stdout.write('Aborted.\n') return } }
await stopSyncServer(config) rmSync(config.stateRoot, { recursive: true, force: true }) process.stdout.write(`Wiped state root: ${config.stateRoot}\n`)}
function readLineFromStdin(): Promise<string> { return new Promise((resolve) => { let buf = '' process.stdin.setEncoding('utf8') const onData = (chunk: string) => { buf += chunk const nl = buf.indexOf('\n') if (nl !== -1) { process.stdin.off('data', onData) process.stdin.pause() resolve(buf.slice(0, nl)) } } process.stdin.resume() process.stdin.on('data', onData) })}
main().catch((err: Error) => { process.stderr.write(`${err.stack ?? err.message}\n`) process.exit(1)})