Installing 0.1.0 failed twice, both my mistakes.
- Node refuses to strip types under node_modules, so shipping .ts for the
CLI could never work from an installed package. tsconfig.cli.json now
compiles those modules to dist/ as ES modules, and the helper scripts
are plain .mjs.
- serverExternalPackages made Turbopack emit require("onvif-<hash>"),
a name that resolves nowhere, because onvif comes from a git URL.
Bundling it fixes that; checked from an installed tarball, where
/api/discover answered 200 and a real camera's info and snapshot came
back through the packaged server.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
129 lines
5.1 KiB
JavaScript
Executable File
129 lines
5.1 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 } 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();
|
|
await applyEnvironment();
|
|
}
|
|
|
|
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);
|
|
}
|