cameras/cli/onvif-dashboard.mjs
Michael Mainguy 65345d0984 Add the first-run experience and the onvif-dashboard command
- src/lib/first-run.ts settles where data lives (asked once, remembered
  in a pointer under the user's config dir), creates the owner-only key
  that protects stored camera logins, and reports whether MediaMTX is
  installed. Without a terminal nothing prompts: defaults are taken and
  logged, so a service still starts. Running it again changes nothing.
- cli/onvif-dashboard.mjs is the published command: setup, install-video
  and help. Node runs the TypeScript in src/lib directly, so the CLI
  needs no build of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 19:00:16 -05:00

88 lines
3.4 KiB
JavaScript
Executable File

#!/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 { createInterface } from "node:readline/promises";
import { parseArgs } from "node:util";
import path from "node:path";
import { applyEnvironment, firstRun, SECRETS_FILE } from "../src/lib/first-run.ts";
import { installMediamtx } from "../src/lib/mediamtx-install.ts";
import { adminFilePath, readAdminFile } from "../src/lib/admin-file.ts";
const HELP = `onvif-dashboard — watch and manage ONVIF cameras on your own network
Usage:
onvif-dashboard setup choose where data lives and prepare this machine
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");
}
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 === "setup") await setup();
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);
}