- 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>
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { mkdtemp, readFile, realpath, rm, stat } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
|
|
/** Runs the published command as a real process, in a directory of its own. */
|
|
const run = promisify(execFile);
|
|
const CLI = path.join(import.meta.dirname, "onvif-dashboard.mjs");
|
|
|
|
let dir: string;
|
|
beforeEach(async () => {
|
|
// realpath because macOS temp folders are symlinks, and the CLI reports resolved paths.
|
|
dir = await realpath(await mkdtemp(path.join(os.tmpdir(), "cli-")));
|
|
});
|
|
afterEach(() => rm(dir, { recursive: true, force: true }));
|
|
|
|
const cli = (args: string[]) =>
|
|
run(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", CLI, ...args], {
|
|
cwd: dir,
|
|
env: {
|
|
...process.env,
|
|
XDG_CONFIG_HOME: path.join(dir, "config"),
|
|
CAMERAS_BIN_DIR: path.join(dir, "bin"),
|
|
CAMERAS_DATA_DIR: undefined,
|
|
},
|
|
}).catch((err: Error & { code?: number; stdout?: string; stderr?: string }) => ({
|
|
code: err.code,
|
|
stdout: err.stdout ?? "",
|
|
stderr: err.stderr ?? "",
|
|
}));
|
|
|
|
describe("onvif-dashboard", () => {
|
|
it("explains itself, and says so when the command is unknown", async () => {
|
|
expect((await cli(["help"])).stdout).toContain("onvif-dashboard setup");
|
|
const bad = (await cli(["wat"])) as { code?: number; stderr: string };
|
|
expect(bad.code).toBe(2);
|
|
expect(bad.stderr).toContain("Unknown command: wat");
|
|
});
|
|
|
|
it("sets a machine up without a terminal: default folder, a key, and what to do next", async () => {
|
|
const { stdout } = await cli(["setup"]);
|
|
const data = path.join(dir, ".data");
|
|
|
|
expect(stdout).toContain("No terminal to ask");
|
|
expect(stdout).toContain(`Data folder: ${data}`);
|
|
expect(stdout).toContain("install-video");
|
|
expect(stdout).toContain("No admin login yet");
|
|
expect(stdout).toContain("onvif-dashboard start");
|
|
|
|
const secrets = path.join(data, "secrets.env");
|
|
expect((await stat(secrets)).mode & 0o777).toBe(0o600);
|
|
expect(await readFile(secrets, "utf8")).toMatch(/CAMERA_CREDENTIALS_KEY=[0-9a-f]{64}/);
|
|
expect(JSON.parse(await readFile(path.join(dir, "config/onvif-dashboard/config.json"), "utf8"))).toEqual({
|
|
dataDir: data,
|
|
});
|
|
});
|
|
|
|
it("running it again changes nothing", async () => {
|
|
await cli(["setup"]);
|
|
const secrets = path.join(dir, ".data", "secrets.env");
|
|
const before = await readFile(secrets, "utf8");
|
|
const { stdout } = await cli(["setup"]);
|
|
expect(await readFile(secrets, "utf8")).toBe(before);
|
|
expect(stdout).not.toContain("Made a key");
|
|
});
|
|
});
|