diff --git a/App.tsx b/App.tsx index 14140fccb..22368f399 100755 --- a/App.tsx +++ b/App.tsx @@ -258,26 +258,57 @@ function App({ settings }: { settings: SettingsState }) { }, [handleSyncNow]); // Update check hook - checks for new versions on startup - const { updateState, openReleasePage, dismissUpdate } = useUpdateCheck(); + const { updateState, openReleasePage, downloadUpdate, installUpdate } = useUpdateCheck(); + + const handleUpdateAction = useCallback(async () => { + if (updateState.updateDownloaded) { + const installed = await installUpdate(); + if (!installed) { + openReleasePage(); + } + return; + } + if (updateState.isDownloading) { + return; + } + const started = await downloadUpdate(); + if (!started) { + openReleasePage(); + } + }, [ + updateState.updateDownloaded, + updateState.isDownloading, + installUpdate, + downloadUpdate, + openReleasePage, + ]); // Show toast notification when update is available useEffect(() => { if (updateState.hasUpdate && updateState.latestRelease) { const version = updateState.latestRelease.version; - toast.info( - t('update.available.message', { version }), - { - title: t('update.available.title'), - duration: 8000, // Show longer for update notifications - onClick: () => { - openReleasePage(); - dismissUpdate(); - }, - actionLabel: t('update.downloadNow'), - } - ); + toast.info(t('update.available.message', { version }), { + title: t('update.available.title'), + duration: 8000, // Show longer for update notifications + onClick: () => { + void handleUpdateAction(); + }, + actionLabel: t('update.downloadNow'), + }); } - }, [updateState.hasUpdate, updateState.latestRelease, t, openReleasePage, dismissUpdate]); + }, [updateState.hasUpdate, updateState.latestRelease, t, handleUpdateAction]); + + useEffect(() => { + if (!updateState.updateDownloaded) return; + toast.success(t('update.downloaded.message'), { + title: t('update.downloaded.title'), + duration: 8000, + onClick: () => { + void handleUpdateAction(); + }, + actionLabel: t('update.installNow'), + }); + }, [updateState.updateDownloaded, t, handleUpdateAction]); // Debounce ref for moveFocus to prevent double-triggering when focus switches const lastMoveFocusTimeRef = useRef(0); diff --git a/application/i18n/locales/en.ts b/application/i18n/locales/en.ts index 6532bc9db..f06924e17 100644 --- a/application/i18n/locales/en.ts +++ b/application/i18n/locales/en.ts @@ -76,10 +76,14 @@ const en: Messages = { 'update.available.title': 'Update Available', 'update.available.message': 'A new version {version} is available. Click to download.', 'update.checking': 'Checking for updates...', + 'update.downloading': 'Downloading update... {percent}%', + 'update.downloaded.title': 'Update Ready', + 'update.downloaded.message': 'Update downloaded. Restart to install.', 'update.upToDate.title': 'Up to Date', 'update.upToDate.message': 'You are running the latest version ({version}).', 'update.error': 'Failed to check for updates', 'update.downloadNow': 'Download Now', + 'update.installNow': 'Install and Restart', 'update.remindLater': 'Remind Later', 'update.skipVersion': 'Skip This Version', diff --git a/application/i18n/locales/zh-CN.ts b/application/i18n/locales/zh-CN.ts index 2ef3d188c..35a29221d 100644 --- a/application/i18n/locales/zh-CN.ts +++ b/application/i18n/locales/zh-CN.ts @@ -64,10 +64,14 @@ const zhCN: Messages = { 'update.available.title': '发现新版本', 'update.available.message': '新版本 {version} 已发布,点击前往下载。', 'update.checking': '正在检查更新...', + 'update.downloading': '正在下载更新... {percent}%', + 'update.downloaded.title': '更新已就绪', + 'update.downloaded.message': '更新已下载完成,重启即可安装。', 'update.upToDate.title': '已是最新版本', 'update.upToDate.message': '当前版本 ({version}) 已是最新。', 'update.error': '检查更新失败', 'update.downloadNow': '立即下载', + 'update.installNow': '安装并重启', 'update.remindLater': '稍后提醒', 'update.skipVersion': '跳过此版本', diff --git a/application/state/useUpdateCheck.ts b/application/state/useUpdateCheck.ts index d023082f8..48edda0ac 100644 --- a/application/state/useUpdateCheck.ts +++ b/application/state/useUpdateCheck.ts @@ -21,11 +21,15 @@ const debugLog = (...args: unknown[]) => { export interface UpdateState { isChecking: boolean; + isDownloading: boolean; + updateDownloaded: boolean; + downloadProgress: number | null; hasUpdate: boolean; currentVersion: string; latestRelease: ReleaseInfo | null; error: string | null; lastCheckedAt: number | null; + autoUpdateSupported: boolean; } export interface UseUpdateCheckResult { @@ -33,8 +37,28 @@ export interface UseUpdateCheckResult { checkNow: () => Promise; dismissUpdate: () => void; openReleasePage: () => void; + downloadUpdate: () => Promise; + installUpdate: () => Promise; } +type UpdateStatusPayload = { + status?: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'; + supported?: boolean; + updateInfo?: { + version?: string; + releaseName?: string; + releaseNotes?: string; + releaseDate?: string; + } | null; + progress?: { + percent?: number; + transferred?: number; + total?: number; + bytesPerSecond?: number; + } | null; + error?: string | null; +}; + /** * Hook for managing update checks * - Automatically checks for updates on startup (with delay) @@ -44,11 +68,15 @@ export interface UseUpdateCheckResult { export function useUpdateCheck(): UseUpdateCheckResult { const [updateState, setUpdateState] = useState({ isChecking: false, + isDownloading: false, + updateDownloaded: false, + downloadProgress: null, hasUpdate: false, currentVersion: '', latestRelease: null, error: null, lastCheckedAt: null, + autoUpdateSupported: false, }); const hasCheckedOnStartupRef = useRef(false); @@ -71,6 +99,85 @@ export function useUpdateCheck(): UseUpdateCheckResult { void loadVersion(); }, []); + useEffect(() => { + const loadUpdateStatus = async () => { + try { + const bridge = netcattyBridge.get(); + const status = await bridge?.getUpdateStatus?.(); + if (!status) return; + setUpdateState((prev) => ({ + ...prev, + autoUpdateSupported: Boolean(status.supported), + isDownloading: status.status === 'downloading', + updateDownloaded: status.status === 'downloaded', + downloadProgress: status.progress?.percent ?? prev.downloadProgress, + error: status.error ?? prev.error, + })); + } catch { + // Ignore if auto updater isn't available + } + }; + void loadUpdateStatus(); + }, []); + + useEffect(() => { + const bridge = netcattyBridge.get(); + if (!bridge?.onUpdateStatus) return; + const unsubscribe = bridge.onUpdateStatus((payload: UpdateStatusPayload) => { + if (!payload) return; + setUpdateState((prev) => { + const next = { ...prev }; + if (typeof payload.supported === 'boolean') { + next.autoUpdateSupported = payload.supported; + } + + if (payload.status === 'checking') { + next.isChecking = true; + next.error = null; + } else if (payload.status === 'available') { + next.isChecking = false; + next.hasUpdate = true; + if (!next.latestRelease && payload.updateInfo?.version) { + next.latestRelease = { + version: payload.updateInfo.version, + tagName: `v${payload.updateInfo.version}`, + name: payload.updateInfo.releaseName || payload.updateInfo.version, + body: payload.updateInfo.releaseNotes || '', + htmlUrl: getReleaseUrl(payload.updateInfo.version), + publishedAt: payload.updateInfo.releaseDate || '', + assets: [], + }; + } + } else if (payload.status === 'downloading') { + next.isDownloading = true; + next.updateDownloaded = false; + next.hasUpdate = true; + next.downloadProgress = payload.progress?.percent ?? next.downloadProgress; + next.error = null; + } else if (payload.status === 'downloaded') { + next.isDownloading = false; + next.updateDownloaded = true; + next.hasUpdate = true; + next.downloadProgress = 100; + next.error = null; + } else if (payload.status === 'error') { + next.isChecking = false; + next.isDownloading = false; + next.error = payload.error || 'Unknown error'; + } else if (payload.status === 'not-available') { + next.isChecking = false; + } + return next; + }); + }); + + return () => { + if (typeof unsubscribe === 'function') { + unsubscribe(); + } + }; + }, []); + const performCheck = useCallback(async (currentVersion: string): Promise => { debugLog('performCheck called', { currentVersion, IS_UPDATE_DEMO_MODE }); @@ -92,6 +199,18 @@ export function useUpdateCheck(): UseUpdateCheckResult { setUpdateState((prev) => ({ ...prev, isChecking: true, error: null })); try { + const bridge = netcattyBridge.get(); + if (bridge?.updateCheck) { + void bridge.updateCheck().then((status) => { + if (status && typeof status.supported === 'boolean') { + setUpdateState((prev) => ({ ...prev, autoUpdateSupported: status.supported })); + } + if (status?.error) { + setUpdateState((prev) => ({ ...prev, error: status.error || 'Unknown error' })); + } + }); + } + let result: UpdateCheckResult; if (IS_UPDATE_DEMO_MODE) { @@ -189,6 +308,55 @@ export function useUpdateCheck(): UseUpdateCheckResult { window.open(url, '_blank', 'noopener,noreferrer'); }, [updateState.latestRelease]); + const downloadUpdate = useCallback(async () => { + const bridge = netcattyBridge.get(); + if (!bridge?.downloadUpdate) return false; + setUpdateState((prev) => ({ ...prev, isDownloading: true, error: null })); + try { + const result = await bridge.downloadUpdate(); + if (result && typeof result.supported === 'boolean') { + setUpdateState((prev) => ({ ...prev, autoUpdateSupported: result.supported })); + } + if (result?.error) { + setUpdateState((prev) => ({ + ...prev, + isDownloading: false, + error: result.error || 'Unknown error', + })); + return false; + } + if (result?.supported === false) { + setUpdateState((prev) => ({ ...prev, isDownloading: false })); + return false; + } + return true; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error'; + setUpdateState((prev) => ({ ...prev, isDownloading: false, error: errorMsg })); + return false; + } + }, []); + + const installUpdate = useCallback(async () => { + const bridge = netcattyBridge.get(); + if (!bridge?.installUpdate) return false; + try { + const result = await bridge.installUpdate(); + if (result?.error) { + setUpdateState((prev) => ({ ...prev, error: result.error || 'Unknown error' })); + return false; + } + if (result?.supported === false) { + return false; + } + return true; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error'; + setUpdateState((prev) => ({ ...prev, error: errorMsg })); + return false; + } + }, []); + // Startup check with delay - runs once on mount useEffect(() => { debugLog('Startup check effect mounted, IS_UPDATE_DEMO_MODE:', IS_UPDATE_DEMO_MODE); @@ -261,5 +429,7 @@ export function useUpdateCheck(): UseUpdateCheckResult { checkNow, dismissUpdate, openReleasePage, + downloadUpdate, + installUpdate, }; } diff --git a/components/SettingsApplicationTab.tsx b/components/SettingsApplicationTab.tsx index 6e51b9314..e0dca50a0 100644 --- a/components/SettingsApplicationTab.tsx +++ b/components/SettingsApplicationTab.tsx @@ -66,7 +66,7 @@ const ActionRow: React.FC<{ export default function SettingsApplicationTab() { const { t } = useI18n(); const { openExternal, getApplicationInfo } = useApplicationBackend(); - const { updateState, checkNow, openReleasePage } = useUpdateCheck(); + const { updateState, checkNow, openReleasePage, downloadUpdate, installUpdate } = useUpdateCheck(); const [appInfo, setAppInfo] = useState({ name: "Netcatty", version: "" }); const [lastCheckResult, setLastCheckResult] = useState<'none' | 'available' | 'upToDate'>('none'); const [hasAutoChecked, setHasAutoChecked] = useState(false); @@ -106,6 +106,25 @@ export default function SettingsApplicationTab() { void checkNow(); }, [hasAutoChecked, updateState.isChecking, isUpdateDemoMode, appInfo.version, checkNow]); + const handleUpdateAction = async () => { + if (updateState.updateDownloaded) { + const installed = await installUpdate(); + if (!installed) { + openReleasePage(); + } + return; + } + + if (updateState.isDownloading) { + return; + } + + const started = await downloadUpdate(); + if (!started) { + openReleasePage(); + } + }; + const handleCheckForUpdates = async () => { // In demo mode, allow checking even for dev builds if (!isUpdateDemoMode && (!appInfo.version || appInfo.version === '0.0.0')) { @@ -120,12 +139,13 @@ export default function SettingsApplicationTab() { if (result?.hasUpdate && result.latestRelease) { setLastCheckResult('available'); - toast.info( - t('update.available.message', { version: result.latestRelease.version }), - t('update.available.title') - ); - // Open the release page - openReleasePage(); + toast.info(t('update.available.message', { version: result.latestRelease.version }), { + title: t('update.available.title'), + onClick: () => { + void handleUpdateAction(); + }, + actionLabel: t('update.downloadNow'), + }); } else if (result) { setLastCheckResult('upToDate'); toast.success( @@ -141,6 +161,15 @@ export default function SettingsApplicationTab() { const issueUrl = useMemo(() => buildIssueUrl(appInfo), [appInfo]); const releasesUrl = `${REPO_URL}/releases`; const discussionsUrl = `${REPO_URL}/discussions`; + const downloadPercent = updateState.downloadProgress != null + ? Math.round(updateState.downloadProgress) + : null; + + const updateActionLabel = updateState.updateDownloaded + ? t('update.installNow') + : updateState.isDownloading + ? t('update.downloading', { percent: downloadPercent ?? 0 }) + : t('update.downloadNow'); return ( @@ -157,15 +186,16 @@ export default function SettingsApplicationTab() { {/* Update available badge - inline with version */} {updateState.hasUpdate && updateState.latestRelease && ( )} diff --git a/electron-builder.json b/electron-builder.json index 88e2ad7f5..4fc2b7d88 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -79,5 +79,12 @@ } ], "category": "Development" - } + }, + "publish": [ + { + "provider": "github", + "owner": "binaricat", + "repo": "Netcatty" + } + ] } diff --git a/electron/bridges/updateBridge.cjs b/electron/bridges/updateBridge.cjs new file mode 100644 index 000000000..3a5d38855 --- /dev/null +++ b/electron/bridges/updateBridge.cjs @@ -0,0 +1,193 @@ +/** + * Update Bridge - Handles auto updates via electron-updater + */ + +let electronModule = null; +let autoUpdater; +let autoUpdaterLoaded = false; +let listenersAttached = false; + +const updateStatus = { + status: "idle", + supported: false, + updateInfo: null, + progress: null, + error: null, +}; + +function loadAutoUpdater() { + if (autoUpdaterLoaded) return autoUpdater; + autoUpdaterLoaded = true; + try { + autoUpdater = require("electron-updater").autoUpdater; + } catch (err) { + console.warn("[UpdateBridge] electron-updater not available:", err?.message || err); + autoUpdater = null; + } + return autoUpdater; +} + +function isSupported() { + return !!loadAutoUpdater() && !!electronModule?.app?.isPackaged; +} + +function normalizeReleaseNotes(notes) { + if (!notes) return ""; + if (typeof notes === "string") return notes; + if (Array.isArray(notes)) { + return notes + .map((entry) => entry?.note || "") + .filter(Boolean) + .join("\n"); + } + return ""; +} + +function mapUpdateInfo(info) { + if (!info) return null; + return { + version: info.version || "", + releaseName: info.releaseName || info.version || "", + releaseNotes: normalizeReleaseNotes(info.releaseNotes), + releaseDate: info.releaseDate || "", + }; +} + +function mapProgress(progress) { + if (!progress) return null; + return { + percent: typeof progress.percent === "number" ? progress.percent : 0, + transferred: typeof progress.transferred === "number" ? progress.transferred : 0, + total: typeof progress.total === "number" ? progress.total : 0, + bytesPerSecond: typeof progress.bytesPerSecond === "number" ? progress.bytesPerSecond : 0, + }; +} + +function broadcastStatus() { + const { BrowserWindow } = electronModule || {}; + if (!BrowserWindow?.getAllWindows) return; + const windows = BrowserWindow.getAllWindows(); + windows.forEach((win) => { + if (!win || win.isDestroyed()) return; + win.webContents?.send?.("netcatty:update:status", updateStatus); + }); +} + +function setStatus(next) { + updateStatus.status = next.status ?? updateStatus.status; + updateStatus.updateInfo = next.updateInfo ?? updateStatus.updateInfo; + updateStatus.progress = next.progress ?? updateStatus.progress; + updateStatus.error = next.error ?? updateStatus.error; + updateStatus.supported = isSupported(); + broadcastStatus(); +} + +function attachListeners() { + if (!isSupported() || listenersAttached) return; + const updater = loadAutoUpdater(); + if (!updater) return; + listenersAttached = true; + + updater.on("checking-for-update", () => { + setStatus({ status: "checking", error: null }); + }); + + updater.on("update-available", (info) => { + setStatus({ status: "available", updateInfo: mapUpdateInfo(info), error: null }); + }); + + updater.on("update-not-available", (info) => { + setStatus({ status: "not-available", updateInfo: mapUpdateInfo(info), error: null }); + }); + + updater.on("download-progress", (progress) => { + setStatus({ status: "downloading", progress: mapProgress(progress), error: null }); + }); + + updater.on("update-downloaded", (info) => { + setStatus({ status: "downloaded", updateInfo: mapUpdateInfo(info), progress: null, error: null }); + }); + + updater.on("error", (err) => { + setStatus({ + status: "error", + error: err?.message || String(err), + }); + }); +} + +function init(deps) { + electronModule = deps?.electronModule || null; + updateStatus.supported = isSupported(); +} + +function registerHandlers(ipcMain) { + if (!ipcMain) return; + const updater = loadAutoUpdater(); + if (updater) { + updater.autoDownload = false; + } + attachListeners(); + + ipcMain.handle("netcatty:update:getStatus", async () => { + updateStatus.supported = isSupported(); + return { ...updateStatus }; + }); + + ipcMain.handle("netcatty:update:check", async () => { + if (!isSupported()) { + return { supported: false, error: "Auto updates are unavailable in this build." }; + } + attachListeners(); + try { + setStatus({ status: "checking", error: null }); + const result = await updater.checkForUpdates(); + const info = mapUpdateInfo(result?.updateInfo); + if (info) { + updateStatus.updateInfo = info; + } + return { supported: true, updateInfo: info }; + } catch (err) { + const message = err?.message || String(err); + setStatus({ status: "error", error: message }); + return { supported: true, error: message }; + } + }); + + ipcMain.handle("netcatty:update:download", async () => { + if (!isSupported()) { + return { supported: false, error: "Auto updates are unavailable in this build." }; + } + attachListeners(); + try { + if (!updateStatus.updateInfo) { + await updater.checkForUpdates(); + } + await updater.downloadUpdate(); + return { supported: true }; + } catch (err) { + const message = err?.message || String(err); + setStatus({ status: "error", error: message }); + return { supported: true, error: message }; + } + }); + + ipcMain.handle("netcatty:update:install", async () => { + if (!isSupported()) { + return { supported: false, error: "Auto updates are unavailable in this build." }; + } + try { + updater.quitAndInstall(); + return { supported: true }; + } catch (err) { + const message = err?.message || String(err); + setStatus({ status: "error", error: message }); + return { supported: true, error: message }; + } + }); +} + +module.exports = { + init, + registerHandlers, +}; diff --git a/electron/main.cjs b/electron/main.cjs index 227cacdef..f3fb55eb6 100644 --- a/electron/main.cjs +++ b/electron/main.cjs @@ -77,6 +77,7 @@ const googleAuthBridge = require("./bridges/googleAuthBridge.cjs"); const onedriveAuthBridge = require("./bridges/onedriveAuthBridge.cjs"); const cloudSyncBridge = require("./bridges/cloudSyncBridge.cjs"); const windowManager = require("./bridges/windowManager.cjs"); +const updateBridge = require("./bridges/updateBridge.cjs"); // GPU settings // NOTE: Do not disable Chromium sandbox by default. @@ -359,6 +360,7 @@ const registerBridges = (win) => { sftpBridge.init(deps); transferBridge.init(deps); terminalBridge.init(deps); + updateBridge.init({ electronModule }); // Register all IPC handlers sshBridge.registerHandlers(ipcMain); @@ -372,6 +374,7 @@ const registerBridges = (win) => { googleAuthBridge.registerHandlers(ipcMain, electronModule); onedriveAuthBridge.registerHandlers(ipcMain, electronModule); cloudSyncBridge.registerHandlers(ipcMain); + updateBridge.registerHandlers(ipcMain); // Settings window handler ipcMain.handle("netcatty:settings:open", async () => { diff --git a/electron/preload.cjs b/electron/preload.cjs index 9eaedad93..12b2e9a06 100644 --- a/electron/preload.cjs +++ b/electron/preload.cjs @@ -9,6 +9,7 @@ const chainProgressListeners = new Map(); const authFailedListeners = new Map(); const languageChangeListeners = new Set(); const fullscreenChangeListeners = new Set(); +const updateStatusListeners = new Set(); ipcRenderer.on("netcatty:data", (_event, payload) => { const set = dataListeners.get(payload.sessionId); @@ -70,6 +71,16 @@ ipcRenderer.on("netcatty:window:fullscreen-changed", (_event, isFullscreen) => { }); }); +ipcRenderer.on("netcatty:update:status", (_event, payload) => { + updateStatusListeners.forEach((cb) => { + try { + cb(payload); + } catch (err) { + console.error("Update status callback failed", err); + } + }); +}); + // Authentication failed events @@ -362,6 +373,10 @@ const api = { fullscreenChangeListeners.add(cb); return () => fullscreenChangeListeners.delete(cb); }, + onUpdateStatus: (cb) => { + updateStatusListeners.add(cb); + return () => updateStatusListeners.delete(cb); + }, // Settings window openSettingsWindow: () => ipcRenderer.invoke("netcatty:settings:open"), @@ -407,6 +422,10 @@ const api = { // App info getAppInfo: () => ipcRenderer.invoke("netcatty:app:getInfo"), + getUpdateStatus: () => ipcRenderer.invoke("netcatty:update:getStatus"), + updateCheck: () => ipcRenderer.invoke("netcatty:update:check"), + downloadUpdate: () => ipcRenderer.invoke("netcatty:update:download"), + installUpdate: () => ipcRenderer.invoke("netcatty:update:install"), // Tell main process the renderer has mounted/painted (used to avoid initial blank screen). rendererReady: () => ipcRenderer.send("netcatty:renderer:ready"), diff --git a/global.d.ts b/global.d.ts index 1e89a7b02..9c380a8b7 100644 --- a/global.d.ts +++ b/global.d.ts @@ -111,6 +111,34 @@ interface PortForwardStatusResult { type PortForwardStatusCallback = (status: 'inactive' | 'connecting' | 'active' | 'error', error?: string) => void; +interface NetcattyUpdateInfo { + version: string; + releaseName?: string; + releaseNotes?: string; + releaseDate?: string; +} + +interface NetcattyUpdateProgress { + percent?: number; + transferred?: number; + total?: number; + bytesPerSecond?: number; +} + +interface NetcattyUpdateStatus { + status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error'; + supported?: boolean; + updateInfo?: NetcattyUpdateInfo | null; + progress?: NetcattyUpdateProgress | null; + error?: string | null; +} + +interface NetcattyUpdateActionResult { + supported: boolean; + updateInfo?: NetcattyUpdateInfo | null; + error?: string; +} + interface NetcattyBridge { startSSHSession(options: NetcattySSHOptions): Promise; startTelnetSession?(options: { @@ -231,6 +259,13 @@ interface NetcattyBridge { windowIsMaximized?(): Promise; windowIsFullscreen?(): Promise; onWindowFullScreenChanged?(cb: (isFullscreen: boolean) => void): () => void; + + // Auto update + getUpdateStatus?(): Promise; + updateCheck?(): Promise; + downloadUpdate?(): Promise; + installUpdate?(): Promise; + onUpdateStatus?(cb: (status: NetcattyUpdateStatus) => void): () => void; // Settings window openSettingsWindow?(): Promise; diff --git a/package-lock.json b/package-lock.json index d801aa0ec..9382e5d8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@xterm/addon-webgl": "^0.18.0", "@xterm/xterm": "^5.5.0", "clsx": "2.1.1", + "electron-updater": "^6.6.4", "lucide-react": "0.560.0", "node-pty": "1.1.0-beta19", "react": "^19.2.1", @@ -6278,7 +6279,6 @@ "version": "2.0.1", "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-hidden": { @@ -7706,6 +7706,70 @@ "dev": true, "license": "ISC" }, + "node_modules/electron-updater": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz", + "integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.5.1", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-winstaller": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", @@ -8791,7 +8855,6 @@ "version": "4.2.11", "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/gtoken": { @@ -9302,7 +9365,6 @@ "version": "4.1.1", "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -9426,7 +9488,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/levn": { @@ -9727,6 +9788,19 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -10941,7 +11015,6 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz", "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/scheduler": { @@ -10954,7 +11027,6 @@ "version": "7.7.3", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11612,6 +11684,12 @@ "semver": "bin/semver" } }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",