onvif-dashboard install-video put MediaMTX in ./bin relative to where it was run, but start launches the server from inside the package, so the app looked for it there and reported it missing. serverEnvironment() now settles CAMERAS_DATA_DIR and CAMERAS_BIN_DIR while still in the user's directory, and start passes them on. Checked from an installed tarball: MediaMTX in ./bin is found and the video bridge comes up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
130 lines
5.2 KiB
JavaScript
Executable File
130 lines
5.2 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 { 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);
|
|
}
|