feat: add auto check update

This commit is contained in:
Puppet
2026-01-05 11:20:45 +08:00
parent ff6fa55829
commit 3c843c448a
11 changed files with 605 additions and 31 deletions
+45 -14
View File
@@ -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<number>(0);
+4
View File
@@ -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',
+4
View File
@@ -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': '跳过此版本',
+170
View File
@@ -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<UpdateCheckResult | null>;
dismissUpdate: () => void;
openReleasePage: () => void;
downloadUpdate: () => Promise<boolean>;
installUpdate: () => Promise<boolean>;
}
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<UpdateState>({
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<UpdateCheckResult | null> => {
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,
};
}
+40 -10
View File
@@ -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<AppInfo>({ 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 (
<SettingsTabContent value="application">
@@ -157,15 +186,16 @@ export default function SettingsApplicationTab() {
{/* Update available badge - inline with version */}
{updateState.hasUpdate && updateState.latestRelease && (
<button
onClick={() => void openReleasePage()}
onClick={() => void handleUpdateAction()}
className={cn(
"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium",
"bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300",
"hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors cursor-pointer"
"hover:bg-blue-200 dark:hover:bg-blue-800 transition-colors cursor-pointer",
updateState.isDownloading && "opacity-70 cursor-default"
)}
>
<ArrowUpCircle size={12} />
v{updateState.latestRelease.version} {t('update.downloadNow')}
v{updateState.latestRelease.version} {updateActionLabel}
</button>
)}
</div>
+8 -1
View File
@@ -79,5 +79,12 @@
}
],
"category": "Development"
}
},
"publish": [
{
"provider": "github",
"owner": "binaricat",
"repo": "Netcatty"
}
]
}
+193
View File
@@ -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,
};
+3
View File
@@ -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 () => {
+19
View File
@@ -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"),
Vendored
+35
View File
@@ -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<string>;
startTelnetSession?(options: {
@@ -231,6 +259,13 @@ interface NetcattyBridge {
windowIsMaximized?(): Promise<boolean>;
windowIsFullscreen?(): Promise<boolean>;
onWindowFullScreenChanged?(cb: (isFullscreen: boolean) => void): () => void;
// Auto update
getUpdateStatus?(): Promise<NetcattyUpdateStatus>;
updateCheck?(): Promise<NetcattyUpdateActionResult>;
downloadUpdate?(): Promise<NetcattyUpdateActionResult>;
installUpdate?(): Promise<NetcattyUpdateActionResult>;
onUpdateStatus?(cb: (status: NetcattyUpdateStatus) => void): () => void;
// Settings window
openSettingsWindow?(): Promise<boolean>;
+84 -6
View File
@@ -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",