From f320caa7249370b478006f4c87f8798e201e0c20 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Fri, 15 May 2026 10:24:30 -0300 Subject: [PATCH] =?UTF-8?q?fix(cli):=20code-review-2=20=E2=80=94=20I1/I2/M?= =?UTF-8?q?1/M2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- bin/cli/commands/keys.mjs | 278 ++++++++++++++++++++++-------------- bin/cli/commands/logs.mjs | 113 ++++++++++++--- bin/cli/commands/tunnel.mjs | 2 +- bin/cli/locales/en.json | 5 +- bin/cli/locales/pt-BR.json | 5 +- 5 files changed, 277 insertions(+), 126 deletions(-) diff --git a/bin/cli/commands/keys.mjs b/bin/cli/commands/keys.mjs index 0d5f572b4a..34a61dbb70 100644 --- a/bin/cli/commands/keys.mjs +++ b/bin/cli/commands/keys.mjs @@ -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; } diff --git a/bin/cli/commands/logs.mjs b/bin/cli/commands/logs.mjs index 6f0ecb1e7c..c8cd5edb1e 100644 --- a/bin/cli/commands/logs.mjs +++ b/bin/cli/commands/logs.mjs @@ -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(); diff --git a/bin/cli/commands/tunnel.mjs b/bin/cli/commands/tunnel.mjs index c6ecede0a0..df07507a66 100644 --- a/bin/cli/commands/tunnel.mjs +++ b/bin/cli/commands/tunnel.mjs @@ -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; } diff --git a/bin/cli/locales/en.json b/bin/cli/locales/en.json index 6bbe91155a..f217dc1866 100644 --- a/bin/cli/locales/en.json +++ b/bin/cli/locales/en.json @@ -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", diff --git a/bin/cli/locales/pt-BR.json b/bin/cli/locales/pt-BR.json index 2a266267c9..6b7cae77d1 100644 --- a/bin/cli/locales/pt-BR.json +++ b/bin/cli/locales/pt-BR.json @@ -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",