mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
feat(cli): add Radar status and sync commands
This commit is contained in:
76
bin/cli/commands/radar.mjs
Normal file
76
bin/cli/commands/radar.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
|
||||
const statusSchema = [
|
||||
{ key: "feed", header: "Feed" },
|
||||
{ key: "available", header: "Available" },
|
||||
{ key: "version", header: "Version" },
|
||||
{ key: "tier", header: "Tier" },
|
||||
{ key: "fetchedAt", header: "Fetched" },
|
||||
];
|
||||
|
||||
const syncSchema = [
|
||||
{ key: "feed", header: "Feed" },
|
||||
{ key: "status", header: "Status" },
|
||||
{ key: "version", header: "Version" },
|
||||
{ key: "reason", header: "Reason" },
|
||||
];
|
||||
|
||||
function exitCodeFor(response) {
|
||||
return Number.isInteger(response.exitCode) ? response.exitCode : response.status === 401 ? 4 : 1;
|
||||
}
|
||||
|
||||
export async function runRadarStatusCommand(opts = {}) {
|
||||
const response = await apiFetch("/api/radar/status", { acceptNotOk: true });
|
||||
if (!response.ok) return exitCodeFor(response);
|
||||
const data = await response.json();
|
||||
if (opts.output === "json") {
|
||||
emit(data, opts);
|
||||
return 0;
|
||||
}
|
||||
const rows = Object.entries(data.feeds ?? {}).map(([feed, value]) => ({
|
||||
feed,
|
||||
...(value && typeof value === "object" ? value : { available: false }),
|
||||
}));
|
||||
emit(rows, opts, statusSchema);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function runRadarSyncCommand(opts = {}) {
|
||||
const response = await apiFetch("/api/radar/sync-all", {
|
||||
method: "POST",
|
||||
body: {},
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (!response.ok) return exitCodeFor(response);
|
||||
const data = await response.json();
|
||||
if (opts.output === "json") {
|
||||
emit(data, opts);
|
||||
return 0;
|
||||
}
|
||||
const rows = Object.entries(data).map(([feed, value]) => ({
|
||||
feed,
|
||||
...(value && typeof value === "object" ? value : { status: "error" }),
|
||||
}));
|
||||
emit(rows, opts, syncSchema);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function registerRadar(program) {
|
||||
const radar = program.command("radar").description(t("radar.description"));
|
||||
radar
|
||||
.command("status")
|
||||
.description(t("radar.status"))
|
||||
.action(async (_opts, command) => {
|
||||
const code = await runRadarStatusCommand(command.optsWithGlobals());
|
||||
if (code !== 0) process.exitCode = code;
|
||||
});
|
||||
radar
|
||||
.command("sync")
|
||||
.description(t("radar.sync"))
|
||||
.action(async (_opts, command) => {
|
||||
const code = await runRadarSyncCommand(command.optsWithGlobals());
|
||||
if (code !== 0) process.exitCode = code;
|
||||
});
|
||||
}
|
||||
@@ -78,6 +78,7 @@ import { registerTokens } from "./tokens.mjs";
|
||||
import { registerConfigure } from "./configure.mjs";
|
||||
import { registerApiCommands } from "../api-commands/registry.mjs";
|
||||
import { registerPlugin } from "./plugin.mjs";
|
||||
import { registerRadar } from "./radar.mjs";
|
||||
|
||||
export function registerCommands(program) {
|
||||
registerMemory(program);
|
||||
@@ -161,4 +162,5 @@ export function registerCommands(program) {
|
||||
registerConfigure(program);
|
||||
registerApiCommands(program);
|
||||
registerPlugin(program);
|
||||
registerRadar(program);
|
||||
}
|
||||
|
||||
@@ -921,6 +921,11 @@
|
||||
"model": "Filter by model"
|
||||
}
|
||||
},
|
||||
"radar": {
|
||||
"description": "Inspect and synchronize the local Radar catalog feeds",
|
||||
"status": "Show local Radar settings and feed cache status",
|
||||
"sync": "Synchronize catalog, referrals, offers, and Intel through the local server"
|
||||
},
|
||||
"resilience": {
|
||||
"description": "Inspect and manage resilience mechanisms",
|
||||
"status": {
|
||||
|
||||
@@ -918,6 +918,11 @@
|
||||
"model": "Filtrar por model"
|
||||
}
|
||||
},
|
||||
"radar": {
|
||||
"description": "Inspecionar e sincronizar os feeds locais do catálogo Radar",
|
||||
"status": "Mostrar configurações locais e estado dos caches do Radar",
|
||||
"sync": "Sincronizar catálogo, indicações, ofertas e Intel pelo servidor local"
|
||||
},
|
||||
"resilience": {
|
||||
"description": "Inspecionar e gerenciar mecanismos de resiliência",
|
||||
"status": {
|
||||
|
||||
97
tests/unit/cli-radar-commands.test.ts
Normal file
97
tests/unit/cli-radar-commands.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
function makeResponse(data: unknown, status = 200): Response {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
headers: new Headers(),
|
||||
json: async () => data,
|
||||
text: async () => JSON.stringify(data),
|
||||
} as Response;
|
||||
}
|
||||
|
||||
async function captureStdout(fn: () => Promise<number>): Promise<{ output: string; code: number }> {
|
||||
const chunks: string[] = [];
|
||||
const original = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = ((chunk: string | Uint8Array) => {
|
||||
if (typeof chunk === "string") chunks.push(chunk);
|
||||
return true;
|
||||
}) as typeof process.stdout.write;
|
||||
try {
|
||||
const code = await fn();
|
||||
return { output: chunks.join(""), code };
|
||||
} finally {
|
||||
process.stdout.write = original;
|
||||
}
|
||||
}
|
||||
|
||||
test("radar status is GET-only, read-only, and prints no secret", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let method = "GET";
|
||||
let url = "";
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
url = String(input);
|
||||
method = init?.method ?? "GET";
|
||||
return makeResponse({
|
||||
settings: { optIn: true, hasSupporterKey: true },
|
||||
feeds: { catalog: { available: true, version: "2026.08.09.1", tier: "live" } },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
const { runRadarStatusCommand } = await import("../../bin/cli/commands/radar.mjs");
|
||||
const result = await captureStdout(() => runRadarStatusCommand({ output: "json" }));
|
||||
assert.equal(result.code, 0);
|
||||
assert.match(url, /\/api\/radar\/status$/);
|
||||
assert.equal(method, "GET");
|
||||
assert.ok(!result.output.includes("omr_"));
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("radar sync posts only to the local aggregate route and prints per-feed results", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let method = "";
|
||||
let url = "";
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
url = String(input);
|
||||
method = init?.method ?? "GET";
|
||||
return makeResponse({
|
||||
catalog: { status: "updated", version: "2026.08.09.1" },
|
||||
referrals: { status: "stale" },
|
||||
offers: { status: "no_key" },
|
||||
intel: { status: "no_key" },
|
||||
});
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
const { runRadarSyncCommand } = await import("../../bin/cli/commands/radar.mjs");
|
||||
const result = await captureStdout(() => runRadarSyncCommand({ output: "json" }));
|
||||
assert.equal(result.code, 0);
|
||||
assert.match(url, /\/api\/radar\/sync-all$/);
|
||||
assert.equal(method, "POST");
|
||||
const parsed = JSON.parse(result.output) as Record<string, unknown>;
|
||||
assert.deepEqual(Object.keys(parsed).sort(), ["catalog", "intel", "offers", "referrals"]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("CLI registry exposes nested radar status and sync commands with EN/PT strings", async () => {
|
||||
const { createProgram } = await import("../../bin/cli/program.mjs");
|
||||
const program = createProgram();
|
||||
const radar = program.commands.find((command) => command.name() === "radar");
|
||||
assert.ok(radar);
|
||||
assert.deepEqual(radar.commands.map((command) => command.name()).sort(), ["status", "sync"]);
|
||||
|
||||
for (const locale of ["en", "pt-BR"]) {
|
||||
const messages = JSON.parse(
|
||||
fs.readFileSync(path.resolve(process.cwd(), `bin/cli/locales/${locale}.json`), "utf8")
|
||||
) as { radar?: Record<string, unknown> };
|
||||
assert.equal(typeof messages.radar?.description, "string");
|
||||
assert.equal(typeof messages.radar?.status, "string");
|
||||
assert.equal(typeof messages.radar?.sync, "string");
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user