diff --git a/cli/README.md b/cli/README.md index 050d996a..979bb669 100644 --- a/cli/README.md +++ b/cli/README.md @@ -82,7 +82,6 @@ That's it! Start coding with FREE AI models. 9router # Start with default settings 9router --port 8080 # Custom port 9router --no-browser # Don't open browser -9router --skip-update # Skip auto-update check 9router --help # Show all options ``` diff --git a/cli/cli.js b/cli/cli.js index 6a0fc064..088d7e87 100755 --- a/cli/cli.js +++ b/cli/cli.js @@ -3,7 +3,6 @@ const { spawn, exec, execSync } = require("child_process"); const path = require("path"); const fs = require("fs"); -const https = require("https"); const net = require("net"); const os = require("os"); @@ -26,42 +25,6 @@ function waitServerReady(port, { timeoutMs = 15000, intervalMs = 150 } = {}) { }); } -// Native spinner - no external dependency -function createSpinner(text) { - const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; - let i = 0; - let interval = null; - let currentText = text; - return { - start() { - if (process.stdout.isTTY) { - process.stdout.write(`\r${frames[0]} ${currentText}`); - interval = setInterval(() => { - process.stdout.write(`\r${frames[i++ % frames.length]} ${currentText}`); - }, 80); - } - return this; - }, - stop() { - if (interval) { - clearInterval(interval); - interval = null; - } - if (process.stdout.isTTY) { - process.stdout.write("\r\x1b[K"); - } - }, - succeed(msg) { - this.stop(); - console.log(`✅ ${msg}`); - }, - fail(msg) { - this.stop(); - console.log(`❌ ${msg}`); - } - }; -} - const pkg = require("./package.json"); const { ensureSqliteRuntime, buildEnvWithRuntime } = require("./hooks/sqliteRuntime"); const { ensureTrayRuntime } = require("./hooks/trayRuntime"); @@ -77,7 +40,6 @@ try { ensureTrayRuntime({ silent: true }); } catch {} // Configuration constants const APP_NAME = pkg.name; // Use from package.json -const INSTALL_CMD_LATEST = `npm i -g ${APP_NAME}@latest --prefer-online`; const DEFAULT_PORT = 20128; const DEFAULT_HOST = "0.0.0.0"; @@ -106,7 +68,6 @@ const PROCESS_IDENTIFIERS = [ let port = DEFAULT_PORT; let host = DEFAULT_HOST; let noBrowser = false; -let skipUpdate = false; let showLog = false; let trayMode = false; @@ -121,8 +82,6 @@ for (let i = 0; i < args.length; i++) { noBrowser = true; } else if (args[i] === "--log" || args[i] === "-l") { showLog = true; - } else if (args[i] === "--skip-update") { - skipUpdate = true; } else if (args[i] === "--tray" || args[i] === "-t") { trayMode = true; process.env.TRAY_MODE = "1"; @@ -136,7 +95,6 @@ Options: -n, --no-browser Don't open browser automatically -l, --log Show server logs (default: hidden) -t, --tray Run in system tray mode (background) - --skip-update Skip auto-update check -h, --help Show this help message -v, --version Show version `); @@ -147,26 +105,9 @@ Options: } } -// Auto-relaunch after update: detached process has no TTY → fallback to tray -if (skipUpdate && !trayMode && !process.stdin.isTTY) { - trayMode = true; - process.env.TRAY_MODE = "1"; -} - // Always use Node.js runtime with absolute path const RUNTIME = process.execPath; -// Compare semver versions: returns 1 if a > b, -1 if a < b, 0 if equal -function compareVersions(a, b) { - const partsA = a.split(".").map(Number); - const partsB = b.split(".").map(Number); - for (let i = 0; i < 3; i++) { - if (partsA[i] > partsB[i]) return 1; - if (partsA[i] < partsB[i]) return -1; - } - return 0; -} - // Get app data dir (matches app/src/lib/dataDir.js convention) function getAppDataDir() { return process.platform === "win32" @@ -437,55 +378,6 @@ function isRestrictedEnvironment() { return null; } -// Check if new version available, return latest version or null -function checkForUpdate() { - return new Promise((resolve) => { - if (skipUpdate) { - resolve(null); - return; - } - - const spinner = createSpinner("Checking for updates...").start(); - let resolved = false; - - const safetyTimeout = setTimeout(() => { - if (!resolved) { - resolved = true; - spinner.stop(); - resolve(null); - } - }, 8000); - - const done = (version) => { - if (resolved) return; - resolved = true; - clearTimeout(safetyTimeout); - spinner.stop(); - resolve(version); - }; - - const req = https.get(`https://registry.npmjs.org/${pkg.name}/latest`, { timeout: 3000 }, (res) => { - let data = ""; - res.on("data", chunk => data += chunk); - res.on("end", () => { - try { - const latest = JSON.parse(data); - if (latest.version && compareVersions(latest.version, pkg.version) > 0) { - done(latest.version); - } else { - done(null); - } - } catch (e) { - done(null); - } - }); - }); - - req.on("error", () => done(null)); - req.on("timeout", () => { req.destroy(); done(null); }); - }); -} - // Open browser function openBrowser(url) { const platform = process.platform; @@ -520,14 +412,12 @@ if (!fs.existsSync(serverPath)) { process.exit(1); } -// Start server immediately; run update check in parallel (not on the critical path). -const updatePromise = checkForUpdate(); killAllAppProcesses(port) .then(() => killProcessOnPort(port)) - .then(() => startServer(updatePromise)); + .then(() => startServer()); // Show interface selection menu -async function showInterfaceMenu(latestVersion) { +async function showInterfaceMenu() { const { selectMenu } = require("./src/cli/utils/input"); const { clearScreen } = require("./src/cli/utils/display"); const { getEndpoint } = require("./src/cli/utils/endpoint"); @@ -549,10 +439,6 @@ async function showInterfaceMenu(latestVersion) { const menuItems = []; - if (latestVersion) { - menuItems.push({ label: `Update to v${latestVersion} (current: v${pkg.version})`, icon: "⬆" }); - } - menuItems.push( { label: "Web UI (Open in Browser)", icon: "🌐" }, { label: "Terminal UI (Interactive CLI)", icon: "💻" }, @@ -562,21 +448,16 @@ async function showInterfaceMenu(latestVersion) { const selected = await selectMenu(`Choose Interface (v${pkg.version})`, menuItems, 0, subtitle); - const offset = latestVersion ? 1 : 0; - - if (latestVersion && selected === 0) return "update"; - if (selected === offset) return "web"; - if (selected === offset + 1) return "terminal"; - if (selected === offset + 2) return "hide"; + if (selected === 0) return "web"; + if (selected === 1) return "terminal"; + if (selected === 2) return "hide"; return "exit"; } const MAX_RESTARTS = 2; const RESTART_RESET_MS = 30000; // Reset counter if alive > 30s -function startServer(updatePromise) { - // Accept either a Promise (parallel update check) or a resolved value. - const latestVersionPromise = Promise.resolve(updatePromise); +function startServer() { const displayHost = getDisplayHost(); const url = `http://${displayHost}:${port}/dashboard`; // Surface real network exposure when bound to all interfaces (default 0.0.0.0). @@ -708,28 +589,14 @@ function startServer(updatePromise) { // Wait for server to be ready, then show interface menu loop + tray waitServerReady(port).then(async () => { - // Resolve parallel update check (already running); don't block server start on it. - const latestVersion = await latestVersionPromise; // Start tray icon alongside TUI initTrayIcon(); try { while (true) { - const choice = await showInterfaceMenu(latestVersion); + const choice = await showInterfaceMenu(); - if (choice === "update") { - isShuttingDown = true; - const { clearScreen } = require("./src/cli/utils/display"); - clearScreen(); - console.log(`\n⬆ Update v${pkg.version} → v${latestVersion}\n`); - console.log(`Run this after exit:\n`); - console.log(` \x1b[33m${INSTALL_CMD_LATEST}\x1b[0m\n`); - cleanup(); - await killAllAppProcesses(port); - await killProcessOnPort(port); - setTimeout(() => process.exit(0), 200); - return; - } else if (choice === "web") { + if (choice === "web") { openBrowser(url); // Wait for user to come back const { pause } = require("./src/cli/utils/input"); @@ -767,7 +634,7 @@ function startServer(updatePromise) { // Windows/Linux: spawn detached bgProcess (systray works fine in child) console.log(`\n⏳ Starting background process... (tray icon will appear in ~3s)`); - const bgProcess = spawn(process.execPath, [__filename, "--tray", "--skip-update", "-p", port.toString()], { + const bgProcess = spawn(process.execPath, [__filename, "--tray", "-p", port.toString()], { detached: true, stdio: "ignore", windowsHide: true, diff --git a/cli/scripts/build-cli.js b/cli/scripts/build-cli.js index a1b28c5f..5483673d 100644 --- a/cli/scripts/build-cli.js +++ b/cli/scripts/build-cli.js @@ -248,17 +248,6 @@ if (fs.existsSync(mitmSrc)) { console.log("⏭️ No MITM files found\n"); } -// Step 7b: Copy standalone updater (headless Node process for install progress) -console.log("7️⃣ b Copying updater files..."); -const updaterSrc = path.join(appDir, "src", "lib", "updater"); -const updaterDest = path.join(cliAppDir, "src", "lib", "updater"); -if (fs.existsSync(updaterSrc)) { - copyRecursive(updaterSrc, updaterDest); - console.log("✅ Copied updater files\n"); -} else { - console.log("⏭️ No updater files found\n"); -} - // Step 8: Build MITM server (config driven - see app/cli/scripts/buildMitm.js) console.log("8️⃣ Building MITM server..."); try { diff --git a/cli/scripts/buildMitm.js b/cli/scripts/buildMitm.js index e47f593a..55cd24f0 100644 --- a/cli/scripts/buildMitm.js +++ b/cli/scripts/buildMitm.js @@ -14,9 +14,7 @@ const cliDir = path.resolve(__dirname, ".."); const appDir = path.resolve(cliDir, ".."); const cliAppDir = process.env.NINEROUTER_CLI_APP_DIR || path.join(cliDir, "app"); const cliMitmDir = path.join(cliAppDir, "src", "mitm"); -// Bundle everything — no externals. This keeps MITM runtime self-contained so -// it can be copied to DATA_DIR/runtime/ and spawned from there (escapes -// node_modules file locks that block `npm i -g 9router@latest` on Windows). +// Bundle everything — no externals. This keeps the MITM runtime self-contained. const EXTERNALS = []; const ENTRIES = ["server.js"]; diff --git a/cli/src/cli/tray/autostart.js b/cli/src/cli/tray/autostart.js index 4ab93cf2..f075847f 100644 --- a/cli/src/cli/tray/autostart.js +++ b/cli/src/cli/tray/autostart.js @@ -170,7 +170,6 @@ function enableMacOS(cliPath) { ${nodePath} ${routerScript} --tray - --skip-update EnvironmentVariables @@ -249,7 +248,7 @@ function enableWindows(cliPath) { // Run node + cli.js directly, hidden window. Avoids the fragile // `9router.cmd` lookup that depended on the npm prefix path. const vbsContent = `Set WshShell = CreateObject("WScript.Shell") -WshShell.Run """${nodePath}"" ""${routerScript}"" --tray --skip-update", 0, False +WshShell.Run """${nodePath}"" ""${routerScript}"" --tray", 0, False `; fs.writeFileSync(vbsPath, vbsContent); return true; @@ -282,7 +281,7 @@ function enableLinux(cliPath) { Type=Application Name=9Router Comment=9Router API Proxy -Exec=${nodePath} ${routerScript} --tray --skip-update +Exec=${nodePath} ${routerScript} --tray Hidden=false NoDisplay=false X-GNOME-Autostart-enabled=true diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/BaseUrlSelect.js b/src/app/(dashboard)/dashboard/cli-tools/components/BaseUrlSelect.js index 58482a9b..36f65006 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/BaseUrlSelect.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/BaseUrlSelect.js @@ -1,7 +1,7 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; -import { UPDATER_CONFIG } from "@/shared/constants/config"; +import { APP_CONFIG } from "@/shared/constants/config"; const STORAGE_KEY = "9router.cliToolEndpointPresets"; const CUSTOM_VALUE = "__custom__"; @@ -33,7 +33,7 @@ const buildOptions = ({ requiresExternalUrl, tunnelEnabled, tunnelPublicUrl, tai const opts = []; const wrap = (url) => (withV1 ? ensureV1(url) : (url || "").replace(/\/+$/, "")); if (!requiresExternalUrl) { - const localUrl = wrap(`http://127.0.0.1:${UPDATER_CONFIG.appPort}`); + const localUrl = wrap(`http://127.0.0.1:${APP_CONFIG.defaultPort}`); opts.push({ value: "local", label: localUrl, url: localUrl }); } if (tunnelEnabled && tunnelPublicUrl) { diff --git a/src/app/(dashboard)/dashboard/profile/page.js b/src/app/(dashboard)/dashboard/profile/page.js index e1d770ae..f6fa6df7 100644 --- a/src/app/(dashboard)/dashboard/profile/page.js +++ b/src/app/(dashboard)/dashboard/profile/page.js @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { Card, Button, Toggle, Input } from "@/shared/components"; -import Modal, { ConfirmModal } from "@/shared/components/Modal"; +import Modal from "@/shared/components/Modal"; import LanguageSwitcher from "@/shared/components/LanguageSwitcher"; import { useTheme } from "@/shared/hooks/useTheme"; import { cn } from "@/shared/utils/cn"; @@ -29,8 +29,6 @@ export default function ProfilePage() { const fetchCurrentUser = useUserStore((state) => state.fetchCurrentUser); const [locale, setLocale] = useState("en"); const [langOpen, setLangOpen] = useState(false); - const [shutdownOpen, setShutdownOpen] = useState(false); - const [isShuttingDown, setIsShuttingDown] = useState(false); const [settings, setSettings] = useState({ fallbackStrategy: "fill-first" }); const [loading, setLoading] = useState(true); const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" }); @@ -257,17 +255,6 @@ export default function ProfilePage() { else if (mode === "import") await runImportDatabase(password); }; - const handleShutdown = async () => { - setIsShuttingDown(true); - try { - await fetch("/api/version/shutdown", { method: "POST" }); - } catch (e) { - // Expected to fail as server shuts down; ignore error - } - setIsShuttingDown(false); - setShutdownOpen(false); - }; - const handleLogout = async () => { try { const res = await fetch("/api/auth/logout", { method: "POST" }); @@ -492,15 +479,6 @@ export default function ProfilePage() { {/* Account actions */}
-
- {updateInfo && ( -
- - ↑ New version available: v{updateInfo.latestVersion} - -
- - -
-
- )} {/* Navigation */} @@ -343,46 +269,6 @@ export default function Sidebar({ onClose }) { - - {/* Update Confirmation Modal */} - setShowUpdateModal(false)} - onConfirm={handleUpdate} - title="Update 9Router" - message={`Show install command for v${updateInfo?.latestVersion || ""}? You can copy it and shutdown to install manually.`} - confirmText="Show Command" - cancelText="Cancel" - variant="primary" - /> - - {/* Disconnected / Updating Overlay */} - {(isDisconnected || isUpdating) && ( -
- {isUpdating ? ( - - ) : ( -
-
- power_off -
-

Server Disconnected

-

The proxy server has been stopped.

- -
- )} -
- )} ); } @@ -390,62 +276,3 @@ export default function Sidebar({ onClose }) { Sidebar.propTypes = { onClose: PropTypes.func, }; - -function ManualUpdatePanel({ latestVersion, installCmd, copied, onCopyAndShutdown, onCancel, countdown, isDisconnected }) { - const isCountingDown = countdown > 0; - return ( -
-
-
- content_copy -
-
-

Update 9Router{latestVersion ? ` to v${latestVersion}` : ""}

-

- {isDisconnected - ? "Server stopped. Paste the command into a terminal to install." - : isCountingDown - ? `Command copied. Server will stop in ${countdown}s...` - : "Click the button below to copy the install command and shutdown."} -

-
-
- -

Install command:

-
- {installCmd} -
- -
    -
  1. Click Copy & Shutdown below.
  2. -
  3. Paste the command into your terminal and press Enter.
  4. -
  5. Run 9router again after install.
  6. -
- - {isDisconnected ? ( - - ) : ( -
- - -
- )} -
- ); -} - -ManualUpdatePanel.propTypes = { - latestVersion: PropTypes.string, - installCmd: PropTypes.string.isRequired, - copied: PropTypes.bool, - onCopyAndShutdown: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, - countdown: PropTypes.number, - isDisconnected: PropTypes.bool, -}; diff --git a/src/shared/constants/config.js b/src/shared/constants/config.js index 0650d086..f6955bd8 100644 --- a/src/shared/constants/config.js +++ b/src/shared/constants/config.js @@ -5,6 +5,7 @@ export const APP_CONFIG = { name: "9Router Proxy", description: "AI Infrastructure Management", version: pkg.version, + defaultPort: 20128, }; // GitHub configuration @@ -13,25 +14,6 @@ export const GITHUB_CONFIG = { donateUrl: "https://9router.com/api/donate", }; -// Updater configuration -export const UPDATER_CONFIG = { - npmPackageName: "9router", - installCmd: "npm i -g 9router", - installCmdLatest: "npm i -g 9router@latest --prefer-online", - shutdownCountdownSec: 3, - exitDelayMs: 500, - statusPort: 20129, - statusPollIntervalMs: 1000, - statusLogTailLines: 8, - installRetries: 3, - installRetryDelayMs: 5000, - lingerAfterDoneMs: 30000, - waitForExitMinMs: 5000, - waitForExitMaxMs: 20000, - waitForExitCheckMs: 500, - appPort: 20128, -}; - // Theme configuration export const THEME_CONFIG = { storageKey: "theme",