fix(cli): code-review-2 — I1/I2/M1/M2

I1 (logs): implementar filtragem runtime das 7 flags registradas mas ignoradas:
  --request-id, --api-key, --combo, --status, --duration-min, --duration-max,
  --export. Filtragem client-side via buildLogFilter(); --export grava jsonl.
I2 (keys): adicionar isServerUp() check + try/catch nos 8 comandos que chamavam
  apiFetch diretamente sem proteção: regenerate/revoke/reveal/usage/policy-show/
  policy-set/expiration-list/rotate. Garante exit code 1 com mensagem amigável
  quando servidor offline.
M1 (tunnel): substituir string hardcoded inglesa em runTunnelStopCommand linha 151
  por t("tunnel.typeRequired").
M2 (logs): substituir "Log stream stopped." e "Log stream error: ..." por t()
  calls; adicionar logs.stopped / logs.streamError / logs.exported a en.json e
  pt-BR.json.
This commit is contained in:
diegosouzapw
2026-05-15 10:24:30 -03:00
parent 69a2b27a33
commit f320caa724
5 changed files with 277 additions and 126 deletions

View File

@@ -344,17 +344,26 @@ export async function runKeysRegenerateCommand(id, opts = {}) {
return 0;
}
}
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/regenerate`, {
method: "POST",
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/regenerate`, {
method: "POST",
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
console.log(t("keys.regenerated", { key: data.key || data.apiKey || "(see dashboard)" }));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
const data = await res.json();
console.log(t("keys.regenerated", { key: data.key || data.apiKey || "(see dashboard)" }));
return 0;
}
export async function runKeysRevokeCommand(id, opts = {}) {
@@ -370,78 +379,114 @@ export async function runKeysRevokeCommand(id, opts = {}) {
return 0;
}
}
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/revoke`, {
method: "POST",
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/revoke`, {
method: "POST",
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
console.log(t("keys.revoked", { id }));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
console.log(t("keys.revoked", { id }));
return 0;
}
export async function runKeysRevealCommand(id, opts = {}) {
process.stderr.write(t("keys.revealWarning") + "\n");
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/reveal`, {
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/reveal`, {
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
console.log(data.key || data.apiKey || "(not available)");
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
const data = await res.json();
console.log(data.key || data.apiKey || "(not available)");
return 0;
}
export async function runKeysUsageCommand(id, opts = {}) {
const limit = opts.limit || "20";
const res = await apiFetch(
`/api/v1/registered-keys/${encodeURIComponent(id)}/usage?limit=${limit}`,
{ retry: false }
);
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
const data = await res.json();
const rows = data.usage || data.requests || data.items || [];
if (rows.length === 0) {
console.log(t("keys.noUsage"));
const limit = opts.limit || "20";
try {
const res = await apiFetch(
`/api/v1/registered-keys/${encodeURIComponent(id)}/usage?limit=${limit}`,
{ retry: false }
);
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
const rows = data.usage || data.requests || data.items || [];
if (rows.length === 0) {
console.log(t("keys.noUsage"));
return 0;
}
for (const r of rows) {
const ts = r.timestamp || r.createdAt || "";
const path = r.path || r.endpoint || "";
const status = r.status || r.statusCode || "";
console.log(` ${ts} ${String(status).padEnd(4)} ${path}`);
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
for (const r of rows) {
const ts = r.timestamp || r.createdAt || "";
const path = r.path || r.endpoint || "";
const status = r.status || r.statusCode || "";
console.log(` ${ts} ${String(status).padEnd(4)} ${path}`);
}
return 0;
}
export async function runKeysPolicyShowCommand(id, opts = {}) {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/policy`, {
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
const data = await res.json();
if (opts.output === "json" || opts.json) {
console.log(JSON.stringify(data, null, 2));
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/policy`, {
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
if (opts.output === "json" || opts.json) {
console.log(JSON.stringify(data, null, 2));
return 0;
}
console.log(t("keys.policy.title") + ` (${id}):`);
console.log(` rate_limit: ${data.rateLimit ?? data.rate_limit ?? "(unset)"}`);
console.log(` max_cost: ${data.maxCost ?? data.max_cost ?? "(unset)"}`);
console.log(
` allowed_models: ${(data.allowedModels ?? data.allowed_models ?? []).join(", ") || "(all)"}`
);
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
console.log(t("keys.policy.title") + ` (${id}):`);
console.log(` rate_limit: ${data.rateLimit ?? data.rate_limit ?? "(unset)"}`);
console.log(` max_cost: ${data.maxCost ?? data.max_cost ?? "(unset)"}`);
console.log(
` allowed_models: ${(data.allowedModels ?? data.allowed_models ?? []).join(", ") || "(all)"}`
);
return 0;
}
export async function runKeysPolicySetCommand(id, opts = {}) {
@@ -454,47 +499,64 @@ export async function runKeysPolicySetCommand(id, opts = {}) {
console.error(t("keys.policy.nothingToSet"));
return 1;
}
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/policy`, {
method: "PATCH",
body,
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/policy`, {
method: "PATCH",
body,
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
console.log(t("keys.policy.updated"));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
console.log(t("keys.policy.updated"));
return 0;
}
export async function runKeysExpirationListCommand(opts = {}) {
const days = Number(opts.days || 30);
const res = await apiFetch(`/api/v1/registered-keys?expiring=true&days=${days}`, {
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
const data = await res.json();
const rows = data.keys || data.items || data;
if (!Array.isArray(rows) || rows.length === 0) {
console.log(t("keys.expiration.none", { days }));
const days = Number(opts.days || 30);
try {
const res = await apiFetch(`/api/v1/registered-keys?expiring=true&days=${days}`, {
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
const rows = data.keys || data.items || data;
if (!Array.isArray(rows) || rows.length === 0) {
console.log(t("keys.expiration.none", { days }));
return 0;
}
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(rows, null, 2));
return 0;
}
console.log(t("keys.expiration.listTitle", { days }));
for (const k of rows) {
const exp = k.expiresAt || k.expires_at || "(unknown)";
console.log(` ${(k.id || "").padEnd(24)} ${(k.name || "").padEnd(20)} expires: ${exp}`);
}
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
if (opts.json || opts.output === "json") {
console.log(JSON.stringify(rows, null, 2));
return 0;
}
console.log(t("keys.expiration.listTitle", { days }));
for (const k of rows) {
const exp = k.expiresAt || k.expires_at || "(unknown)";
console.log(` ${(k.id || "").padEnd(24)} ${(k.name || "").padEnd(20)} expires: ${exp}`);
}
return 0;
}
export async function runKeysRotateCommand(id, opts = {}) {
@@ -510,20 +572,28 @@ export async function runKeysRotateCommand(id, opts = {}) {
return 0;
}
}
if (!(await isServerUp())) {
console.error(t("common.serverOffline"));
return 1;
}
const gracePeriod = Number(opts.gracePeriod || 60000);
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/rotate`, {
method: "POST",
body: { gracePeriodMs: gracePeriod },
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/rotate`, {
method: "POST",
body: { gracePeriodMs: gracePeriod },
acceptNotOk: true,
retry: false,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
const data = await res.json();
const newId = data.newKeyId || data.id || "(see dashboard)";
console.log(t("keys.rotated", { id, newId }));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
const data = await res.json();
const newId = data.newKeyId || data.id || "(see dashboard)";
console.log(t("keys.rotated", { id, newId }));
return 0;
}

View File

@@ -1,4 +1,4 @@
import { printInfo, printError } from "../io.mjs";
import { writeFileSync, appendFileSync, existsSync, unlinkSync } from "node:fs";
import { t } from "../i18n.mjs";
export function registerLogs(program) {
@@ -24,18 +24,66 @@ export function registerLogs(program) {
});
}
function buildLogFilter(opts) {
const levelFilters = opts.filter ? opts.filter.split(",").map((f) => f.trim()) : [];
const requestId = opts.requestId;
const apiKey = opts.apiKey;
const combo = opts.combo;
const statusFilter = opts.status != null ? String(opts.status) : null;
const durationMin = opts.durationMin != null ? Number(opts.durationMin) : null;
const durationMax = opts.durationMax != null ? Number(opts.durationMax) : null;
return function matchesLog(parsed) {
if (levelFilters.length > 0) {
const level = String(parsed.level || "info").toLowerCase();
if (!levelFilters.includes(level)) return false;
}
if (requestId) {
const rid = String(parsed.requestId || parsed.request_id || "");
if (!rid.includes(requestId)) return false;
}
if (apiKey) {
const key = String(parsed.apiKey || parsed.api_key || parsed.key || "");
if (!key.includes(apiKey)) return false;
}
if (combo) {
const c = String(parsed.combo || parsed.comboName || parsed.combo_name || "");
if (!c.includes(combo)) return false;
}
if (statusFilter) {
const s = String(parsed.status || parsed.statusCode || parsed.status_code || "");
if (!s.startsWith(statusFilter)) return false;
}
if (durationMin != null) {
const d = Number(parsed.duration || parsed.durationMs || parsed.latency || 0);
if (d < durationMin) return false;
}
if (durationMax != null) {
const d = Number(parsed.duration || parsed.durationMs || parsed.latency || 0);
if (d > durationMax) return false;
}
return true;
};
}
export async function runLogsCommand(opts = {}) {
const baseUrl = opts.baseUrl || opts["base-url"] || "http://localhost:20128";
const follow = opts.follow ?? false;
const filter = opts.filter ?? "";
const lines = opts.lines || "100";
const timeout = parseInt(String(opts.timeout || "30000"), 10);
const isJson = opts.output === "json";
const exportPath = opts.export;
const filters = filter ? filter.split(",").map((f) => f.trim()) : [];
// Prepare export file
if (exportPath && existsSync(exportPath)) {
unlinkSync(exportPath);
}
const matchesLog = buildLogFilter(opts);
// Pass only level filters to the stream (server-side); other filters are client-side
const levelFilters = opts.filter ? opts.filter.split(",").map((f) => f.trim()) : [];
const { createLogStream } = await import("../../../src/lib/cli-helper/log-streamer.js");
const { stream, stop } = createLogStream({ baseUrl, filters, follow, timeout });
const { stream, stop } = createLogStream({ baseUrl, filters: levelFilters, follow, timeout });
const reader = stream.getReader();
const decoder = new TextDecoder();
@@ -43,21 +91,43 @@ export async function runLogsCommand(opts = {}) {
const processLine = (line) => {
if (!line.trim()) return;
if (isJson) {
console.log(line);
let parsed = null;
try {
parsed = JSON.parse(line);
} catch {
// Non-JSON line: only include if no structured filters active
if (
opts.requestId ||
opts.apiKey ||
opts.combo ||
opts.status ||
opts.durationMin != null ||
opts.durationMax != null
)
return;
if (exportPath) appendFileSync(exportPath, line + "\n", "utf8");
else console.log(line);
return;
}
try {
const parsed = JSON.parse(line);
const level = parsed.level || "info";
const ts = parsed.timestamp || new Date().toISOString();
const msg = parsed.message || JSON.stringify(parsed);
const prefix =
{ error: "\x1b[31m[ERR]", warn: "\x1b[33m[WRN]", info: "\x1b[36m[INF]" }[level] || "[INF]";
console.log(`${prefix}\x1b[0m ${ts} ${msg}`);
} catch {
console.log(line);
if (!matchesLog(parsed)) return;
if (exportPath) {
appendFileSync(exportPath, JSON.stringify(parsed) + "\n", "utf8");
return;
}
if (isJson) {
console.log(JSON.stringify(parsed));
return;
}
const level = parsed.level || "info";
const ts = parsed.timestamp || new Date().toISOString();
const msg = parsed.message || JSON.stringify(parsed);
const prefix =
{ error: "\x1b[31m[ERR]", warn: "\x1b[33m[WRN]", info: "\x1b[36m[INF]" }[level] || "[INF]";
console.log(`${prefix}\x1b[0m ${ts} ${msg}`);
};
try {
@@ -70,11 +140,16 @@ export async function runLogsCommand(opts = {}) {
for (const line of parts) processLine(line);
}
if (buffer) processLine(buffer);
if (exportPath) console.log(t("logs.exported", { path: exportPath }));
} catch (err) {
if (err.name === "AbortError") {
printInfo("Log stream stopped.");
console.log(t("logs.stopped"));
} else {
printError(`Log stream error: ${err.message}`);
console.error(
t("logs.streamError", {
message: (err instanceof Error ? err.message : String(err)).slice(0, 100),
})
);
}
} finally {
stop();

View File

@@ -148,7 +148,7 @@ export async function runTunnelCreateCommand(type = "cloudflare", opts = {}) {
export async function runTunnelStopCommand(type, opts = {}) {
if (!type) {
console.error("Tunnel type required. Valid: " + VALID_TUNNEL_TYPES.join(", "));
console.error(t("tunnel.typeRequired"));
return 1;
}

View File

@@ -1177,7 +1177,10 @@
"status": "Filter by HTTP status code",
"durationMin": "Min request duration in ms",
"durationMax": "Max request duration in ms",
"export": "Save logs to file (json/jsonl/csv)"
"export": "Save logs to file (json/jsonl/csv)",
"exported": "Logs saved to {path}",
"stopped": "Log stream stopped.",
"streamError": "Log stream error: {message}"
},
"tray": {
"description": "Control the system tray icon",

View File

@@ -1177,7 +1177,10 @@
"status": "Filtrar por código HTTP",
"durationMin": "Duração mínima do request em ms",
"durationMax": "Duração máxima do request em ms",
"export": "Salvar logs em arquivo (json/jsonl/csv)"
"export": "Salvar logs em arquivo (json/jsonl/csv)",
"exported": "Logs salvos em {path}",
"stopped": "Stream de logs interrompido.",
"streamError": "Erro no stream de logs: {message}"
},
"tray": {
"description": "Controlar o ícone na bandeja do sistema",