#!/usr/bin/env node // The command people get from npm (vrek iss-kz5c88a). Node runs the TypeScript in src/lib // directly (type stripping, Node 24+), so there is nothing to build for the CLI itself. import { spawn } from "node:child_process"; import { access } from "node:fs/promises"; import { createInterface } from "node:readline/promises"; import { parseArgs } from "node:util"; import path from "node:path"; import { applyEnvironment, firstRun, SECRETS_FILE, serverEnvironment } from "../dist/lib/first-run.js"; import { installMediamtx } from "../dist/lib/mediamtx-install.js"; import { adminFilePath, readAdminFile } from "../dist/lib/admin-file.js"; const HELP = `onvif-dashboard — watch and manage ONVIF cameras on your own network Usage: onvif-dashboard start run the dashboard (PORT, HOSTNAME) onvif-dashboard setup choose where data lives and prepare this machine onvif-dashboard admin create or replace the admin login (--force to replace) onvif-dashboard install-video download MediaMTX, needed for live video and recording onvif-dashboard help this message Environment: CAMERAS_DATA_DIR where data lives (asked once, then remembered) ADMIN_AUTH_FILE, CAMERA_REGISTRY_FILE, CAMERA_CREDENTIALS_FILE, AUDIT_LOG_FILE, RECORDINGS_DIR, MEDIAMTX_BIN override individual paths `; /** Asks a question when there's a terminal; returns undefined when there isn't. */ function prompter() { if (!process.stdin.isTTY || !process.stdout.isTTY) return undefined; return async (question, fallback) => { const rl = createInterface({ input: process.stdin, output: process.stdout }); try { return await rl.question(`${question} [${fallback}] `); } finally { rl.close(); } }; } async function setup() { const ask = prompter(); const result = await firstRun({ ask }); console.log(`\nData folder: ${result.dataDir}`); console.log(`Key for stored camera logins: ${path.join(result.dataDir, SECRETS_FILE)}`); if (!result.videoReady) { const install = ask ? await ask("Download MediaMTX now? Live video and recording need it. (y/n)", "y") : "n"; if (/^y/i.test(install || "y")) await installVideo(); else console.log("Skipped. Run onvif-dashboard install-video when you want live video."); } else { console.log("Live video: MediaMTX is installed."); } await applyEnvironment(); const admin = await readAdminFile(adminFilePath()).catch(() => null); console.log( admin ? `\nAdmin login: already set up (${adminFilePath()}).` : `\nNo admin login yet. Start the app and open it in a browser: it prints a one-time setup code` + ` in this console, which you enter to create the login. You can skip that, but then anyone on` + ` your network can use it.`, ); console.log("\nReady. Start it with: onvif-dashboard start"); } /** Runs the built server, with the remembered folder and stored key in its environment. */ async function start() { await applyEnvironment(); if (!process.env.CAMERA_CREDENTIALS_KEY) { // Never started before: take the defaults quietly rather than refuse to run. await firstRun(); } // Settle every path now: the server runs from inside the package, not from here. await serverEnvironment(); const server = path.join(import.meta.dirname, "..", ".next", "standalone", "server.js"); if (!(await access(server).then(() => true, () => false))) { console.error("The built server is missing. From a source checkout, run: npm run build"); process.exit(1); } const child = spawn(process.execPath, [server], { cwd: path.dirname(server), stdio: "inherit", env: { ...process.env, PORT: process.env.PORT ?? "3000", HOSTNAME: process.env.HOSTNAME ?? "0.0.0.0" }, }); for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => child.kill(signal)); child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0))); } /** Creates the admin login, prompting for it; the script it runs has the interactive bits. */ async function admin(args) { await applyEnvironment(); const script = path.join(import.meta.dirname, "..", "scripts", "create-admin.mjs"); const child = spawn(process.execPath, [script, ...args], { stdio: "inherit", env: process.env, }); child.on("exit", (code, signal) => process.exit(signal ? 1 : (code ?? 0))); } async function installVideo() { const result = await installMediamtx(); console.log( result.status === "installed" ? `Installed MediaMTX ${result.version} at ${result.binary} (checksum verified).` : `MediaMTX ${result.version} is already installed at ${result.binary}.`, ); } const { positionals } = parseArgs({ allowPositionals: true, strict: false }); const command = positionals[0] ?? "help"; try { if (command === "start") await start(); else if (command === "setup") await setup(); else if (command === "admin") await admin(process.argv.slice(3)); else if (command === "install-video") await installVideo(); else if (command === "help" || command === "--help" || command === "-h") console.log(HELP); else { console.error(`Unknown command: ${command}\n`); console.log(HELP); process.exit(2); } } catch (err) { console.error(err instanceof Error ? err.message : err); process.exit(1); }