From b3cfac3c14a1a660305edba42c8f5818ebd3b625 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Fri, 15 May 2026 04:07:20 -0300 Subject: [PATCH] =?UTF-8?q?feat(cli):=20fase=208.8=20=E2=80=94=20system=20?= =?UTF-8?q?tray=20+=20autostart=20(omniroute=20serve=20--tray)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bin/cli/tray/index.mjs: initTray/killTray/isTrayActive/isTraySupported - bin/cli/tray/traySystray.mjs: systray2 para macOS/Linux (graceful fallback) - bin/cli/tray/trayWindows.mjs: PowerShell NotifyIcon (sem binário extra) - bin/cli/tray/autostart.mjs: launchd (macOS), reg (Windows), .desktop (Linux) - bin/cli/commands/tray.mjs: subcomandos show/hide/quit - bin/cli/commands/autostart.mjs: subcomandos enable/disable/status - serve.mjs: flags --tray/--no-tray, integração após servidor iniciar - i18n: chaves tray.*, autostart.*, serve.tray/no_tray em en.json e pt-BR.json - check-env-doc-sync: DISPLAY e WAYLAND_DISPLAY adicionados ao allowlist (sinais do host OS) - 8 testes unitários cobrindo autostart por plataforma e importação dos módulos --- bin/cli/commands/autostart.mjs | 39 +++++++ bin/cli/commands/registry.mjs | 4 + bin/cli/commands/serve.mjs | 63 ++++++++++-- bin/cli/commands/tray.mjs | 36 +++++++ bin/cli/locales/en.json | 16 ++- bin/cli/locales/pt-BR.json | 16 ++- bin/cli/tray/autostart.mjs | 147 +++++++++++++++++++++++++++ bin/cli/tray/index.mjs | 26 +++++ bin/cli/tray/traySystray.mjs | 107 +++++++++++++++++++ bin/cli/tray/trayWindows.mjs | 75 ++++++++++++++ scripts/check/check-env-doc-sync.mjs | 3 + tests/unit/cli-tray.test.ts | 75 ++++++++++++++ 12 files changed, 599 insertions(+), 8 deletions(-) create mode 100644 bin/cli/commands/autostart.mjs create mode 100644 bin/cli/commands/tray.mjs create mode 100644 bin/cli/tray/autostart.mjs create mode 100644 bin/cli/tray/index.mjs create mode 100644 bin/cli/tray/traySystray.mjs create mode 100644 bin/cli/tray/trayWindows.mjs create mode 100644 tests/unit/cli-tray.test.ts diff --git a/bin/cli/commands/autostart.mjs b/bin/cli/commands/autostart.mjs new file mode 100644 index 0000000000..de77f2d701 --- /dev/null +++ b/bin/cli/commands/autostart.mjs @@ -0,0 +1,39 @@ +import { t } from "../i18n.mjs"; +import { emit } from "../output.mjs"; + +export function registerAutostart(program) { + const cmd = program + .command("autostart") + .description(t("autostart.description") || "Manage OmniRoute autostart at login"); + + cmd + .command("enable") + .description(t("autostart.enable") || "Enable autostart at login") + .action(async (opts, c) => { + const globalOpts = c.optsWithGlobals(); + const { enable } = await import("../tray/autostart.mjs"); + const ok = enable(); + emit({ enabled: ok }, globalOpts); + if (!ok) process.exit(1); + }); + + cmd + .command("disable") + .description(t("autostart.disable") || "Disable autostart at login") + .action(async (opts, c) => { + const globalOpts = c.optsWithGlobals(); + const { disable } = await import("../tray/autostart.mjs"); + const ok = disable(); + emit({ disabled: ok }, globalOpts); + if (!ok) process.exit(1); + }); + + cmd + .command("status") + .description(t("autostart.status") || "Show autostart status") + .action(async (opts, c) => { + const globalOpts = c.optsWithGlobals(); + const { isAutostartEnabled } = await import("../tray/autostart.mjs"); + emit({ enabled: isAutostartEnabled() }, globalOpts); + }); +} diff --git a/bin/cli/commands/registry.mjs b/bin/cli/commands/registry.mjs index e8dda2b2ed..5cd3daf025 100644 --- a/bin/cli/commands/registry.mjs +++ b/bin/cli/commands/registry.mjs @@ -52,6 +52,8 @@ import { registerEnv } from "./env.mjs"; import { registerTestProvider } from "./test-provider.mjs"; import { registerCompletion } from "./completion.mjs"; import { registerRuntime } from "./runtime.mjs"; +import { registerTray } from "./tray.mjs"; +import { registerAutostart } from "./autostart.mjs"; export function registerCommands(program) { registerMemory(program); @@ -109,4 +111,6 @@ export function registerCommands(program) { registerTestProvider(program); registerCompletion(program); registerRuntime(program); + registerTray(program); + registerAutostart(program); } diff --git a/bin/cli/commands/serve.mjs b/bin/cli/commands/serve.mjs index 5ad8e41e5d..85fc2c095e 100644 --- a/bin/cli/commands/serve.mjs +++ b/bin/cli/commands/serve.mjs @@ -26,6 +26,8 @@ export function registerServe(program) { .option("--log", t("serve.log")) .option("--no-recovery", t("serve.no_recovery")) .option("--max-restarts ", t("serve.max_restarts"), parseInt, 2) + .option("--tray", t("serve.tray") || "Show system tray icon (desktop only)") + .option("--no-tray", t("serve.no_tray") || "Disable system tray icon") .action(async (opts) => { await runServe(opts); }); @@ -128,6 +130,7 @@ export async function runServe(opts = {}) { }; const isDaemon = opts.daemon === true; + const useTray = opts.tray === true; if (isDaemon) { return runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort); @@ -145,7 +148,8 @@ export async function runServe(opts = {}) { apiPort, noOpen, opts.log === true, - opts.maxRestarts ?? 2 + opts.maxRestarts ?? 2, + useTray ); } @@ -229,7 +233,8 @@ async function runWithSupervisor( apiPort, noOpen, showLog, - maxRestarts + maxRestarts, + useTray = false ) { if (showLog) process.env.OMNIROUTE_SHOW_LOG = "1"; @@ -253,16 +258,62 @@ async function runWithSupervisor( supervisor.start(); - process.on("SIGINT", () => supervisor.stop()); - process.on("SIGTERM", () => supervisor.stop()); + process.on("SIGINT", () => { + killTrayIfActive(); + supervisor.stop(); + }); + process.on("SIGTERM", () => { + killTrayIfActive(); + supervisor.stop(); + }); if (!showLog) { - waitForServer(dashboardPort, 20000).then((up) => { - if (up) onReady(dashboardPort, apiPort, noOpen); + waitForServer(dashboardPort, 20000).then(async (up) => { + if (up) { + if (useTray) await maybeStartTray(dashboardPort, apiPort, supervisor); + onReady(dashboardPort, apiPort, noOpen); + } }); } } +let _killTray = null; +function killTrayIfActive() { + if (_killTray) { + try { + _killTray(); + } catch {} + _killTray = null; + } +} + +async function maybeStartTray(port, apiPort, supervisor) { + try { + const { initTray, isTraySupported } = await import("../tray/index.mjs"); + if (!isTraySupported()) return; + const { default: open } = await import("open").catch(() => ({ default: null })); + const dashboardUrl = `http://localhost:${port}`; + const tray = initTray({ + port, + onQuit: () => { + killTrayIfActive(); + supervisor.stop(); + }, + onOpenDashboard: () => open?.(dashboardUrl), + onShowLogs: () => { + // In-place: open logs stream (best-effort) + process.stdout.write(`[omniroute][tray] Logs at: ${dashboardUrl}/logs\n`); + }, + }); + if (tray) { + const { killTray } = await import("../tray/index.mjs"); + _killTray = killTray; + } + } catch { + // tray is optional — do not fail the server + } +} + async function onReady(dashboardPort, apiPort, noOpen) { const dashboardUrl = `http://localhost:${dashboardPort}`; const apiUrl = `http://localhost:${apiPort}`; diff --git a/bin/cli/commands/tray.mjs b/bin/cli/commands/tray.mjs new file mode 100644 index 0000000000..76e273bc3c --- /dev/null +++ b/bin/cli/commands/tray.mjs @@ -0,0 +1,36 @@ +import { t } from "../i18n.mjs"; + +export function registerTray(program) { + const cmd = program + .command("tray") + .description(t("tray.description") || "Control the system tray icon"); + + cmd + .command("show") + .description(t("tray.show") || "Show the tray icon (if server is running with --tray)") + .action(() => { + process.stderr.write( + "The tray is managed by `omniroute serve --tray`. Start the server with --tray to enable it.\n" + ); + }); + + cmd + .command("hide") + .description(t("tray.hide") || "Hide the tray icon") + .action(() => { + process.stderr.write( + "Send SIGUSR1 to the serve process to toggle the tray, or restart without --tray.\n" + ); + }); + + cmd + .command("quit") + .description(t("tray.quit") || "Quit OmniRoute via tray") + .action(async () => { + const { default: pidUtils } = await import("../utils/pid.mjs").catch(() => ({ + default: null, + })); + process.stderr.write("Use `omniroute stop` to stop the server.\n"); + process.exit(0); + }); +} diff --git a/bin/cli/locales/en.json b/bin/cli/locales/en.json index 41791e8ce6..bab7948ad8 100644 --- a/bin/cli/locales/en.json +++ b/bin/cli/locales/en.json @@ -174,7 +174,9 @@ "daemon": "Run server as a background daemon", "log": "Show server logs inline", "no_recovery": "Disable auto-restart on crash (debugging mode)", - "max_restarts": "Max crash restarts within 30s before giving up (default: 2)" + "max_restarts": "Max crash restarts within 30s before giving up (default: 2)", + "tray": "Show system tray icon (desktop only, opt-in)", + "no_tray": "Disable system tray icon" }, "backup": { "title": "Backup", @@ -1083,6 +1085,18 @@ "durationMax": "Max request duration in ms", "export": "Save logs to file (json/jsonl/csv)" }, + "tray": { + "description": "Control the system tray icon", + "show": "Show the tray icon (if server is running with --tray)", + "hide": "Hide the tray icon", + "quit": "Quit OmniRoute via tray" + }, + "autostart": { + "description": "Manage OmniRoute autostart at login", + "enable": "Enable autostart at login", + "disable": "Disable autostart at login", + "status": "Show autostart status" + }, "runtime": { "description": "Manage native runtime dependencies", "check": "Check status of native deps in runtime directory", diff --git a/bin/cli/locales/pt-BR.json b/bin/cli/locales/pt-BR.json index 352b1bb20b..6aef7154f7 100644 --- a/bin/cli/locales/pt-BR.json +++ b/bin/cli/locales/pt-BR.json @@ -174,7 +174,9 @@ "daemon": "Executar servidor em segundo plano", "log": "Exibir logs do servidor inline", "no_recovery": "Desabilitar reinício automático em crash (modo debug)", - "max_restarts": "Máximo de reinícios em 30s antes de desistir (padrão: 2)" + "max_restarts": "Máximo de reinícios em 30s antes de desistir (padrão: 2)", + "tray": "Mostrar ícone na bandeja do sistema (apenas desktop, opt-in)", + "no_tray": "Desabilitar ícone na bandeja do sistema" }, "backup": { "title": "Backup", @@ -1083,6 +1085,18 @@ "durationMax": "Duração máxima do request em ms", "export": "Salvar logs em arquivo (json/jsonl/csv)" }, + "tray": { + "description": "Controlar o ícone na bandeja do sistema", + "show": "Mostrar o ícone na bandeja (se o servidor estiver rodando com --tray)", + "hide": "Ocultar o ícone na bandeja", + "quit": "Encerrar OmniRoute via bandeja" + }, + "autostart": { + "description": "Gerenciar inicialização automática do OmniRoute no login", + "enable": "Habilitar inicialização automática no login", + "disable": "Desabilitar inicialização automática no login", + "status": "Mostrar status de inicialização automática" + }, "runtime": { "description": "Gerenciar dependências nativas do runtime", "check": "Verificar status das deps nativas no diretório de runtime", diff --git a/bin/cli/tray/autostart.mjs b/bin/cli/tray/autostart.mjs new file mode 100644 index 0000000000..8ecb59eb7d --- /dev/null +++ b/bin/cli/tray/autostart.mjs @@ -0,0 +1,147 @@ +import { existsSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs"; +import { execSync } from "node:child_process"; +import { homedir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const APP_LABEL = "com.omniroute.autostart"; +const WIN_REG_VALUE = "OmniRoute"; + +function resolveCliPath() { + return ( + process.argv[1] || join(dirname(fileURLToPath(import.meta.url)), "..", "..", "omniroute.mjs") + ); +} + +export function enable() { + if (process.platform === "darwin") return enableMac(); + if (process.platform === "win32") return enableWin(); + if (process.platform === "linux") return enableLinux(); + return false; +} + +export function disable() { + if (process.platform === "darwin") return disableMac(); + if (process.platform === "win32") return disableWin(); + if (process.platform === "linux") return disableLinux(); + return false; +} + +export function isAutostartEnabled() { + if (process.platform === "darwin") return isEnabledMac(); + if (process.platform === "win32") return isEnabledWin(); + if (process.platform === "linux") return isEnabledLinux(); + return false; +} + +function enableMac() { + const plistDir = join(homedir(), "Library", "LaunchAgents"); + mkdirSync(plistDir, { recursive: true }); + const plistPath = join(plistDir, `${APP_LABEL}.plist`); + const cliPath = resolveCliPath(); + const plist = ` + + + Label${APP_LABEL} + ProgramArguments + ${process.execPath} + ${cliPath} + serve + --tray + --no-open + + RunAtLoad + KeepAlive +`; + writeFileSync(plistPath, plist, { mode: 0o644 }); + try { + execSync("launchctl load -w " + JSON.stringify(plistPath), { stdio: "ignore" }); + } catch {} + return existsSync(plistPath); +} + +function disableMac() { + const plistPath = join(homedir(), "Library", "LaunchAgents", `${APP_LABEL}.plist`); + try { + execSync("launchctl unload -w " + JSON.stringify(plistPath), { stdio: "ignore" }); + } catch {} + try { + unlinkSync(plistPath); + } catch {} + return !existsSync(plistPath); +} + +function isEnabledMac() { + return existsSync(join(homedir(), "Library", "LaunchAgents", `${APP_LABEL}.plist`)); +} + +function enableWin() { + const cliPath = resolveCliPath(); + const value = `"${process.execPath}" "${cliPath}" serve --tray --no-open`; + try { + execSync( + `reg add HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v ${WIN_REG_VALUE} /t REG_SZ /d "${value}" /f`, + { stdio: "ignore", windowsHide: true } + ); + return true; + } catch { + return false; + } +} + +function disableWin() { + try { + execSync( + `reg delete HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v ${WIN_REG_VALUE} /f`, + { stdio: "ignore", windowsHide: true } + ); + return true; + } catch { + return false; + } +} + +function isEnabledWin() { + try { + const out = execSync( + `reg query HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v ${WIN_REG_VALUE}`, + { stdio: "pipe", windowsHide: true, encoding: "utf8" } + ); + return out.includes(WIN_REG_VALUE); + } catch { + return false; + } +} + +function enableLinux() { + const dir = join(homedir(), ".config", "autostart"); + mkdirSync(dir, { recursive: true }); + const cliPath = resolveCliPath(); + const desktop = + [ + "[Desktop Entry]", + "Type=Application", + "Name=OmniRoute", + "Comment=AI proxy router with auto fallback", + `Exec=${process.execPath} ${cliPath} serve --tray --no-open`, + "Terminal=false", + "Hidden=false", + "X-GNOME-Autostart-enabled=true", + ].join("\n") + "\n"; + writeFileSync(join(dir, "omniroute.desktop"), desktop, { mode: 0o644 }); + return true; +} + +function disableLinux() { + const desktopPath = join(homedir(), ".config", "autostart", "omniroute.desktop"); + try { + unlinkSync(desktopPath); + return true; + } catch { + return false; + } +} + +function isEnabledLinux() { + return existsSync(join(homedir(), ".config", "autostart", "omniroute.desktop")); +} diff --git a/bin/cli/tray/index.mjs b/bin/cli/tray/index.mjs new file mode 100644 index 0000000000..ab68812591 --- /dev/null +++ b/bin/cli/tray/index.mjs @@ -0,0 +1,26 @@ +import { isTraySupported, initSystrayUnix, killSystrayUnix } from "./traySystray.mjs"; +import { initWinTray, killWinTray } from "./trayWindows.mjs"; + +let active = null; + +export { isTraySupported }; + +export function initTray({ port, onQuit, onOpenDashboard, onShowLogs }) { + if (!isTraySupported()) return null; + const ctx = { port, onQuit, onOpenDashboard, onShowLogs }; + active = process.platform === "win32" ? initWinTray(ctx) : initSystrayUnix(ctx); + return active; +} + +export function killTray() { + if (!active) return; + try { + if (process.platform === "win32") killWinTray(active); + else killSystrayUnix(active); + } catch {} + active = null; +} + +export function isTrayActive() { + return active !== null; +} diff --git a/bin/cli/tray/traySystray.mjs b/bin/cli/tray/traySystray.mjs new file mode 100644 index 0000000000..bf0b7caee0 --- /dev/null +++ b/bin/cli/tray/traySystray.mjs @@ -0,0 +1,107 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { isAutostartEnabled } from "./autostart.mjs"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const MENU_INDEX = { STATUS: 0, DASHBOARD: 1, LOGS: 2, AUTOSTART: 3, QUIT: 4 }; + +export function isTraySupported() { + const p = process.platform; + if (!["darwin", "linux", "win32"].includes(p)) return false; + if (p === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false; + return true; +} + +function loadSystray2() { + const candidates = [ + () => { + const { createRequire } = require("module"); + const req = createRequire(import.meta.url); + return req("systray2").default; + }, + ]; + for (const attempt of candidates) { + try { + return attempt(); + } catch {} + } + return null; +} + +function getIconBase64() { + const iconPath = join(__dirname, "icons", "icon.png"); + if (existsSync(iconPath)) return readFileSync(iconPath).toString("base64"); + return ""; +} + +export function initSystrayUnix({ port, onQuit, onOpenDashboard, onShowLogs }) { + const SysTray = loadSystray2(); + if (!SysTray) return null; + + const autostartEnabled = isAutostartEnabled(); + const items = [ + { title: `OmniRoute • port ${port}`, tooltip: "Server running", enabled: false }, + { title: "Open Dashboard", enabled: true }, + { title: "Show Logs", enabled: true }, + { + title: autostartEnabled ? "✓ Auto-start (click to disable)" : "Enable Auto-start", + enabled: true, + }, + { title: "Quit OmniRoute", enabled: true }, + ]; + + let tray; + try { + tray = new SysTray({ + menu: { + icon: getIconBase64(), + isTemplateIcon: process.platform === "darwin", + title: "", + tooltip: `OmniRoute — port ${port}`, + items, + }, + debug: false, + copyDir: false, + }); + } catch { + return null; + } + + tray.onClick(async (action) => { + if (action.seq_id === MENU_INDEX.DASHBOARD) { + onOpenDashboard?.(); + } else if (action.seq_id === MENU_INDEX.LOGS) { + onShowLogs?.(); + } else if (action.seq_id === MENU_INDEX.AUTOSTART) { + const { enable, disable, isAutostartEnabled: isEnabled } = await import("./autostart.mjs"); + const wasOn = isEnabled(); + if (wasOn) disable(); + else enable(); + const nowOn = !wasOn; + tray.sendAction({ + type: "update-item", + item: { + title: nowOn ? "✓ Auto-start (click to disable)" : "Enable Auto-start", + enabled: true, + }, + seq_id: MENU_INDEX.AUTOSTART, + }); + } else if (action.seq_id === MENU_INDEX.QUIT) { + onQuit?.(); + } + }); + + tray.ready().catch((err) => { + process.stderr.write(`[omniroute][tray] systray2 failed: ${err?.message ?? String(err)}\n`); + }); + + return tray; +} + +export function killSystrayUnix(tray) { + try { + tray.kill(false); + } catch {} +} diff --git a/bin/cli/tray/trayWindows.mjs b/bin/cli/tray/trayWindows.mjs new file mode 100644 index 0000000000..f85de21faf --- /dev/null +++ b/bin/cli/tray/trayWindows.mjs @@ -0,0 +1,75 @@ +import { spawn } from "node:child_process"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { writeFileSync, unlinkSync, existsSync } from "node:fs"; + +const OMNIROUTE_IPC_PORT_BASE = 29128; + +export function initWinTray({ port, onQuit, onOpenDashboard, onShowLogs }) { + if (process.platform !== "win32") return null; + + const ipcPort = OMNIROUTE_IPC_PORT_BASE + (port % 1000); + const scriptPath = join(tmpdir(), `omniroute-tray-${process.pid}.ps1`); + + const ps1 = ` +Add-Type -AssemblyName System.Windows.Forms +Add-Type -AssemblyName System.Drawing + +$tray = New-Object System.Windows.Forms.NotifyIcon +$tray.Text = "OmniRoute - Port ${port}" +$tray.Icon = [System.Drawing.SystemIcons]::Application +$tray.Visible = $true + +$menu = New-Object System.Windows.Forms.ContextMenuStrip + +$mDash = $menu.Items.Add("Open Dashboard") +$mDash.add_Click({ [System.Net.Sockets.TcpClient]::new("127.0.0.1", ${ipcPort}).Close(); Write-Host "DASHBOARD" }) + +$mLogs = $menu.Items.Add("Show Logs") +$mLogs.add_Click({ Write-Host "LOGS" }) + +$mAutostart = $menu.Items.Add("Enable Auto-start") +$mAutostart.add_Click({ Write-Host "AUTOSTART" }) + +$mQuit = $menu.Items.Add("Quit OmniRoute") +$mQuit.add_Click({ Write-Host "QUIT"; [System.Windows.Forms.Application]::Exit() }) + +$tray.ContextMenuStrip = $menu +[System.Windows.Forms.Application]::Run() +$tray.Dispose() +`.trim(); + + writeFileSync(scriptPath, ps1, "utf8"); + + const proc = spawn("powershell.exe", ["-NonInteractive", "-File", scriptPath], { + stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, + detached: false, + shell: false, + }); + + proc.stdout.on("data", async (data) => { + const line = data.toString().trim(); + if (line === "DASHBOARD") onOpenDashboard?.(); + else if (line === "LOGS") onShowLogs?.(); + else if (line === "AUTOSTART") { + const { enable, disable, isAutostartEnabled } = await import("./autostart.mjs"); + if (isAutostartEnabled()) disable(); + else enable(); + } else if (line === "QUIT") onQuit?.(); + }); + + proc.on("exit", () => { + try { + if (existsSync(scriptPath)) unlinkSync(scriptPath); + } catch {} + }); + + return proc; +} + +export function killWinTray(proc) { + try { + proc.kill("SIGTERM"); + } catch {} +} diff --git a/scripts/check/check-env-doc-sync.mjs b/scripts/check/check-env-doc-sync.mjs index d2ea86cb0b..2320e937c9 100644 --- a/scripts/check/check-env-doc-sync.mjs +++ b/scripts/check/check-env-doc-sync.mjs @@ -77,6 +77,9 @@ const IGNORE_FROM_CODE = new Set([ "REPL_SLUG", "WSL_DISTRO_NAME", "WSL_INTEROP", + // X11/Wayland display server vars used by tray heuristic (isTraySupported). + "DISPLAY", + "WAYLAND_DISPLAY", // Aliases for documented vars handled via fallback ordering. "API_KEY", "APP_URL", diff --git a/tests/unit/cli-tray.test.ts b/tests/unit/cli-tray.test.ts new file mode 100644 index 0000000000..00b670e9fb --- /dev/null +++ b/tests/unit/cli-tray.test.ts @@ -0,0 +1,75 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +let tmpDir: string; +let origHome: string | undefined; + +test.before(() => { + tmpDir = mkdtempSync(join(tmpdir(), "omniroute-tray-test-")); + origHome = process.env.HOME; + // Redirecionar HOME para tmpDir para isolar testes de autostart + process.env.HOME = tmpDir; +}); + +test.after(() => { + if (origHome === undefined) delete process.env.HOME; + else process.env.HOME = origHome; + try { + rmSync(tmpDir, { recursive: true, force: true }); + } catch {} +}); + +test("tray/index.mjs pode ser importado sem erro", async () => { + const mod = await import("../../bin/cli/tray/index.mjs"); + assert.equal(typeof mod.initTray, "function"); + assert.equal(typeof mod.killTray, "function"); + assert.equal(typeof mod.isTrayActive, "function"); + assert.equal(typeof mod.isTraySupported, "function"); +}); + +test("isTraySupported retorna boolean", async () => { + const { isTraySupported } = await import("../../bin/cli/tray/index.mjs"); + assert.equal(typeof isTraySupported(), "boolean"); +}); + +test("isTrayActive retorna false antes de iniciar", async () => { + const { isTrayActive } = await import("../../bin/cli/tray/index.mjs"); + assert.equal(isTrayActive(), false); +}); + +test("tray/autostart.mjs pode ser importado sem erro", async () => { + const mod = await import("../../bin/cli/tray/autostart.mjs"); + assert.equal(typeof mod.enable, "function"); + assert.equal(typeof mod.disable, "function"); + assert.equal(typeof mod.isAutostartEnabled, "function"); +}); + +test("autostart.isAutostartEnabled retorna boolean", async () => { + const { isAutostartEnabled } = await import("../../bin/cli/tray/autostart.mjs"); + const result = isAutostartEnabled(); + assert.equal(typeof result, "boolean"); + assert.equal(result, false, "autostart não deve estar habilitado em tmpDir isolado"); +}); + +test("autostart.enable cria arquivo de configuração no Linux", async () => { + if (process.platform !== "linux") return; + const { enable, isAutostartEnabled, disable } = await import("../../bin/cli/tray/autostart.mjs"); + const ok = enable(); + assert.equal(ok, true, "enable deve retornar true"); + assert.equal(isAutostartEnabled(), true, "isAutostartEnabled deve ser true após enable"); + disable(); + assert.equal(isAutostartEnabled(), false, "isAutostartEnabled deve ser false após disable"); +}); + +test("commands/tray.mjs pode ser importado sem erro", async () => { + const mod = await import("../../bin/cli/commands/tray.mjs"); + assert.equal(typeof mod.registerTray, "function"); +}); + +test("commands/autostart.mjs pode ser importado sem erro", async () => { + const mod = await import("../../bin/cli/commands/autostart.mjs"); + assert.equal(typeof mod.registerAutostart, "function"); +});