versions -> update and check for updates on app boot

This commit is contained in:
Egor 2025-08-19 00:02:38 -07:00
parent 45a095d20b
commit 6894614b54
13 changed files with 579 additions and 53 deletions

View file

@ -55,10 +55,10 @@
"@types/react-dom": "^19.1.7",
"@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^8.40.0",
"@vitejs/plugin-react": "^5.0.0",
"@vitejs/plugin-react": "^5.0.1",
"cross-env": "^10.0.0",
"cspell": "^9.2.0",
"electron": "^37.3.0",
"electron": "^37.3.1",
"electron-builder": "^26.0.12",
"electron-vite": "^4.0.0",
"eslint": "^9.33.0",
@ -75,7 +75,7 @@
"prettier": "^3.6.2",
"rollup-plugin-visualizer": "^6.0.3",
"typescript": "^5.9.2",
"vite": "^7.1.2",
"vite": "^7.1.3",
"wait-on": "^8.0.4"
},
"dependencies": {

View file

@ -4,11 +4,15 @@ import { DownloadScreen } from '@/components/screens/Download';
import { LaunchScreen } from '@/components/screens/Launch';
import { InterfaceScreen } from '@/components/screens/Interface';
import { UpdateDialog } from '@/components/UpdateDialog';
import { UpdateAvailableModal } from '@/components/UpdateAvailableModal';
import { SettingsModal } from '@/components/settings/SettingsModal';
import { ScreenTransition } from '@/components/ScreenTransition';
import { AppHeader } from '@/components/AppHeader';
import { useUpdateChecker } from '@/hooks/useUpdateChecker';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import { UI } from '@/constants';
import type { UpdateInfo } from '@/types';
import type { DownloadItem } from '@/types/electron';
type Screen = 'download' | 'launch' | 'interface';
@ -24,6 +28,19 @@ export const App = () => {
const [isImageGenerationMode, setIsImageGenerationMode] =
useState<boolean>(false);
const {
updateInfo: binaryUpdateInfo,
showUpdateModal,
checkForUpdates,
dismissUpdate,
} = useUpdateChecker();
const {
handleDownload: sharedHandleDownload,
downloading,
downloadProgress,
} = useKoboldVersions();
useEffect(() => {
const checkInstallation = async () => {
try {
@ -54,6 +71,12 @@ export const App = () => {
}
setHasInitialized(true);
if (versions.length > 0) {
setTimeout(() => {
checkForUpdates();
}, 2000);
}
} catch (error) {
window.electronAPI.logs.logError(
'Error checking installation:',
@ -86,7 +109,28 @@ export const App = () => {
cleanupInstallDirListener();
cleanupVersionsListener();
};
}, []);
}, [checkForUpdates]);
const handleBinaryUpdate = async (download: DownloadItem) => {
try {
const downloadType = download.type === 'rocm' ? 'rocm' : 'asset';
const success = await sharedHandleDownload(
downloadType,
download,
true,
true
);
if (success) {
dismissUpdate();
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to update binary:',
error as Error
);
}
};
const handleDownloadComplete = async () => {
try {
@ -235,6 +279,22 @@ export const App = () => {
onAccept={handleUpdateAccept}
/>
)}
{showUpdateModal && binaryUpdateInfo && (
<UpdateAvailableModal
opened={showUpdateModal}
onClose={dismissUpdate}
currentVersion={binaryUpdateInfo.currentVersion}
availableUpdate={binaryUpdateInfo.availableUpdate}
onUpdate={handleBinaryUpdate}
isDownloading={
downloading === binaryUpdateInfo.availableUpdate.name
}
downloadProgress={
downloadProgress[binaryUpdateInfo.availableUpdate.name] || 0
}
/>
)}
</AppShell.Main>
<SettingsModal
opened={settingsOpened}

View file

@ -24,8 +24,11 @@ interface DownloadCardProps {
isDownloading?: boolean;
downloadProgress?: number;
disabled?: boolean;
hasUpdate?: boolean;
newerVersion?: string;
onDownload?: (e: MouseEvent<HTMLButtonElement>) => void;
onMakeCurrent?: () => void;
onUpdate?: (e: MouseEvent<HTMLButtonElement>) => void;
}
export const DownloadCard = ({
@ -39,10 +42,35 @@ export const DownloadCard = ({
isDownloading = false,
downloadProgress = 0,
disabled = false,
hasUpdate = false,
newerVersion,
onDownload,
onMakeCurrent,
onUpdate,
}: DownloadCardProps) => {
const renderActionButton = () => {
if (hasUpdate && onUpdate) {
return (
<Button
variant="filled"
size="xs"
onClick={onUpdate}
loading={isDownloading}
disabled={disabled}
color="orange"
leftSection={
isDownloading ? (
<Loader size="1rem" />
) : (
<Download style={{ width: rem(14), height: rem(14) }} />
)
}
>
{isDownloading ? 'Updating...' : `Update to ${newerVersion}`}
</Button>
);
}
if (!isInstalled && onDownload) {
return (
<Button
@ -99,6 +127,11 @@ export const DownloadCard = ({
Recommended
</Badge>
)}
{hasUpdate && (
<Badge variant="light" color="orange" size="sm">
Update Available
</Badge>
)}
</Group>
{description && (
<Text size="xs" c="dimmed" mb="xs">

View file

@ -0,0 +1,192 @@
import {
Modal,
Stack,
Text,
Group,
Button,
Card,
Loader,
useMantineColorScheme,
Anchor,
Progress,
} from '@mantine/core';
import { Download, X, ExternalLink } from 'lucide-react';
import { useState } from 'react';
import type { InstalledVersion, DownloadItem } from '@/types/electron';
import { getDisplayNameFromPath } from '@/utils/versionUtils';
import { GITHUB_API } from '@/constants';
interface UpdateAvailableModalProps {
opened: boolean;
onClose: () => void;
currentVersion: InstalledVersion;
availableUpdate: DownloadItem;
onUpdate: (download: DownloadItem) => Promise<void>;
isDownloading?: boolean;
downloadProgress?: number;
}
export const UpdateAvailableModal = ({
opened,
onClose,
currentVersion,
availableUpdate,
onUpdate,
isDownloading = false,
downloadProgress = 0,
}: UpdateAvailableModalProps) => {
const [isUpdating, setIsUpdating] = useState(false);
const { colorScheme } = useMantineColorScheme();
const handleUpdate = async () => {
try {
setIsUpdating(true);
await onUpdate(availableUpdate);
onClose();
} catch (error) {
window.electronAPI.logs.logError('Failed to update:', error as Error);
} finally {
setIsUpdating(false);
}
};
return (
<Modal
opened={opened}
onClose={onClose}
size="sm"
title="A newer version is available"
centered
closeOnClickOutside={false}
closeOnEscape={!isDownloading && !isUpdating}
>
<Stack gap="md">
<Card
withBorder
radius="md"
p="md"
bg={colorScheme === 'dark' ? 'dark.6' : 'gray.0'}
style={{
borderColor:
colorScheme === 'dark'
? 'var(--mantine-color-orange-7)'
: 'var(--mantine-color-orange-3)',
borderWidth: '2px',
}}
>
<Stack gap="xs">
<Group gap="md" align="center">
<div>
<Text size="xs" c="dimmed">
Current Version
</Text>
<Text fw={500} size="sm">
{currentVersion.version}
</Text>
</div>
<Text
size="lg"
c={colorScheme === 'dark' ? 'orange.4' : 'orange.6'}
fw={500}
>
</Text>
<div>
<Text size="xs" c="dimmed">
Available Version
</Text>
<Text
fw={500}
size="sm"
c={colorScheme === 'dark' ? 'orange.4' : 'orange.6'}
>
{availableUpdate.version}
</Text>
</div>
</Group>
<Text size="xs" c="dimmed">
Binary: {getDisplayNameFromPath(currentVersion)}
</Text>
<Group gap="xs" align="center" mt="xs">
<Text size="xs" c="dimmed">
View release notes:
</Text>
<Anchor
size="xs"
href={`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate.version}`}
target="_blank"
rel="noopener noreferrer"
onClick={() =>
window.electronAPI.app.openExternal(
`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate.version}`
)
}
>
<Group gap={4} align="center">
<span>v{availableUpdate.version}</span>
<ExternalLink size={12} />
</Group>
</Anchor>
</Group>
</Stack>
</Card>
<Stack gap="xs" style={{ minHeight: '44px' }}>
<Progress
value={isDownloading ? Math.min(downloadProgress, 100) : 0}
color="orange"
radius="xl"
style={{
opacity: isDownloading || isUpdating ? 1 : 0,
transition: 'opacity 200ms ease',
}}
/>
<Text
size="xs"
c="dimmed"
ta="center"
style={{
opacity: isDownloading || isUpdating ? 1 : 0,
transition: 'opacity 200ms ease',
}}
>
{isDownloading
? `${Math.min(downloadProgress, 100).toFixed(1)}% complete`
: 'Preparing update...'}
</Text>
</Stack>
<Group justify="flex-end" gap="sm">
<Button
variant="outline"
onClick={onClose}
disabled={isDownloading || isUpdating}
leftSection={<X size={16} />}
>
Skip
</Button>
<Button
onClick={handleUpdate}
loading={isDownloading || isUpdating}
disabled={isDownloading || isUpdating}
leftSection={
isDownloading || isUpdating ? (
<Loader size="1rem" />
) : (
<Download size={16} />
)
}
color="orange"
>
{isDownloading || isUpdating ? 'Updating...' : 'Update Now'}
</Button>
</Group>
</Stack>
</Modal>
);
};

View file

@ -52,7 +52,12 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
setDownloadingAsset(type === 'asset' ? download!.name : null);
try {
const success = await sharedHandleDownload(type, download);
const success = await sharedHandleDownload(
type,
download,
false,
false
);
if (success) {
onDownloadComplete();

View file

@ -21,6 +21,31 @@ import { getDisplayNameFromPath, formatFileSizeInMB } from '@/utils';
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';
const compareVersions = (versionA: string, versionB: string): number => {
const cleanVersion = (version: string): string =>
version.replace(/^v/, '').replace(/[^0-9.]/g, '');
const parseVersion = (version: string): number[] =>
cleanVersion(version)
.split('.')
.map((num) => parseInt(num, 10) || 0);
const a = parseVersion(versionA);
const b = parseVersion(versionB);
const maxLength = Math.max(a.length, b.length);
for (let i = 0; i < maxLength; i++) {
const aVal = a[i] || 0;
const bVal = b[i] || 0;
if (aVal !== bVal) {
return aVal - bVal;
}
}
return 0;
};
interface VersionInfo {
name: string;
version: string;
@ -30,6 +55,8 @@ interface VersionInfo {
downloadUrl?: string;
installedPath?: string;
isROCm?: boolean;
hasUpdate?: boolean;
newerVersion?: string;
}
export const VersionsTab = () => {
@ -68,6 +95,7 @@ export const VersionsTab = () => {
if (currentBinaryPath && versions.length > 0) {
const current = versions.find((v) => v.path === currentBinaryPath);
if (current) {
setCurrentVersion(current);
} else {
@ -79,6 +107,7 @@ export const VersionsTab = () => {
}
} else if (versions.length > 0) {
setCurrentVersion(versions[0]);
await window.electronAPI.config.set(
'currentKoboldBinary',
versions[0].path
@ -119,13 +148,15 @@ export const VersionsTab = () => {
const getAllVersions = (): VersionInfo[] => {
const versions: VersionInfo[] = [];
const processedInstalled = new Set<string>();
availableDownloads.forEach((download) => {
const installedVersion = installedVersions.find((v) => {
const displayName = getDisplayNameFromPath(v);
const downloadBaseName = download.name
.replace(/\.(tar\.gz|zip|exe)$/i, '')
.replace(/\.packed$/, '');
const installedVersion = installedVersions.find((v) => {
const displayName = getDisplayNameFromPath(v);
return displayName === downloadBaseName;
});
@ -135,23 +166,43 @@ export const VersionsTab = () => {
currentVersion.path === installedVersion.path
);
if (installedVersion) {
processedInstalled.add(installedVersion.path);
const hasUpdate =
compareVersions(
download.version || 'unknown',
installedVersion.version
) > 0;
versions.push({
name: download.name,
version: installedVersion?.version || download.version || 'unknown',
size: installedVersion ? undefined : download.size,
isInstalled: Boolean(installedVersion),
version: installedVersion.version,
size: undefined,
isInstalled: true,
isCurrent,
downloadUrl: download.url,
installedPath: installedVersion?.path,
installedPath: installedVersion.path,
isROCm: download.type === 'rocm',
hasUpdate,
newerVersion: hasUpdate ? download.version : undefined,
});
} else {
versions.push({
name: download.name,
version: download.version || 'unknown',
size: download.size,
isInstalled: false,
isCurrent: false,
downloadUrl: download.url,
isROCm: download.type === 'rocm',
});
}
});
installedVersions.forEach((installed) => {
if (!processedInstalled.has(installed.path)) {
const displayName = getDisplayNameFromPath(installed);
const existsInRemote = versions.some((v) => v.name === displayName);
if (!existsInRemote) {
const isCurrent = Boolean(
currentVersion && currentVersion.path === installed.path
);
@ -179,7 +230,12 @@ export const VersionsTab = () => {
}
const downloadType = download.type === 'rocm' ? 'rocm' : 'asset';
const success = await sharedHandleDownload(downloadType, download);
const success = await sharedHandleDownload(
downloadType,
download,
false,
false
);
if (success) {
await loadInstalledVersions();
@ -189,6 +245,30 @@ export const VersionsTab = () => {
}
};
const handleUpdate = async (version: VersionInfo) => {
try {
const download = availableDownloads.find((d) => d.name === version.name);
if (!download) {
throw new Error('Download not found');
}
const downloadType = download.type === 'rocm' ? 'rocm' : 'asset';
const wasCurrentBinary = version.isCurrent;
const success = await sharedHandleDownload(
downloadType,
download,
true,
wasCurrentBinary
);
if (success) {
await loadInstalledVersions();
}
} catch (error) {
window.electronAPI.logs.logError('Failed to update:', error as Error);
}
};
const makeCurrent = async (version: VersionInfo) => {
if (!version.installedPath) return;
@ -310,6 +390,8 @@ export const VersionsTab = () => {
isDownloading={isDownloading}
downloadProgress={downloadProgress[version.name]}
disabled={downloading !== null}
hasUpdate={version.hasUpdate}
newerVersion={version.newerVersion}
onDownload={
!version.isInstalled
? (e) => {
@ -318,8 +400,16 @@ export const VersionsTab = () => {
}
: undefined
}
onUpdate={
version.hasUpdate
? (e) => {
e.stopPropagation();
handleUpdate(version);
}
: undefined
}
onMakeCurrent={
version.isInstalled && !version.isCurrent
version.isInstalled && !version.isCurrent && !version.hasUpdate
? () => makeCurrent(version)
: undefined
}

View file

@ -14,6 +14,7 @@ export const GITHUB_API = {
BASE_URL: 'https://api.github.com',
KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
FRIENDLY_KOBOLD_REPO: 'lone-cloud/friendly-kobold',
get LATEST_RELEASE_URL() {
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_REPO}/releases/latest`;
},

View file

@ -17,7 +17,9 @@ interface UseKoboldVersionsReturn {
loadRemoteVersions: () => Promise<void>;
handleDownload: (
type: 'asset' | 'rocm',
item?: DownloadItem
item?: DownloadItem,
isUpdate?: boolean,
wasCurrentBinary?: boolean
) => Promise<boolean>;
setDownloading: (value: string | null) => void;
setDownloadProgress: (
@ -113,7 +115,12 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
}, [platformInfo.platform]);
const handleDownload = useCallback(
async (type: 'asset' | 'rocm', item?: DownloadItem): Promise<boolean> => {
async (
type: 'asset' | 'rocm',
item?: DownloadItem,
isUpdate = false,
wasCurrentBinary = false
): Promise<boolean> => {
if (type === 'asset' && !item) return false;
const downloadName = item?.name || 'download';
@ -130,6 +137,8 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
browser_download_url: item!.url,
size: item!.size,
created_at: new Date().toISOString(),
isUpdate,
wasCurrentBinary,
});
return result.success !== false;

View file

@ -0,0 +1,115 @@
import { useState, useCallback } from 'react';
import { getDisplayNameFromPath } from '@/utils/versionUtils';
import type { InstalledVersion, DownloadItem } from '@/types/electron';
const compareVersions = (versionA: string, versionB: string): number => {
const cleanVersion = (version: string): string =>
version.replace(/^v/, '').replace(/[^0-9.]/g, '');
const parseVersion = (version: string): number[] =>
cleanVersion(version)
.split('.')
.map((num) => parseInt(num, 10) || 0);
const a = parseVersion(versionA);
const b = parseVersion(versionB);
const maxLength = Math.max(a.length, b.length);
for (let i = 0; i < maxLength; i++) {
const aVal = a[i] || 0;
const bVal = b[i] || 0;
if (aVal !== bVal) {
return aVal - bVal;
}
}
return 0;
};
interface UpdateInfo {
currentVersion: InstalledVersion;
availableUpdate: DownloadItem;
}
export const useUpdateChecker = () => {
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
const [isChecking, setIsChecking] = useState(false);
const [showUpdateModal, setShowUpdateModal] = useState(false);
const checkForUpdates = useCallback(async () => {
setIsChecking(true);
try {
const [currentBinaryPath, installedVersions, releases, rocm] =
await Promise.all([
window.electronAPI.config.get(
'currentKoboldBinary'
) as Promise<string>,
window.electronAPI.kobold.getInstalledVersions(),
window.electronAPI.kobold.getLatestRelease(),
window.electronAPI.kobold.getROCmDownload(),
]);
if (!currentBinaryPath || installedVersions.length === 0) {
return;
}
const currentVersion = installedVersions.find(
(v: InstalledVersion) => v.path === currentBinaryPath
);
if (!currentVersion) {
return;
}
const availableDownloads: DownloadItem[] = [...releases];
if (rocm) {
availableDownloads.push(rocm);
}
const currentDisplayName = getDisplayNameFromPath(currentVersion);
const matchingDownload = availableDownloads.find(
(download: DownloadItem) => {
const downloadBaseName = download.name
.replace(/\.(tar\.gz|zip|exe)$/i, '')
.replace(/\.packed$/, '');
return downloadBaseName === currentDisplayName;
}
);
if (matchingDownload && matchingDownload.version) {
const hasUpdate =
compareVersions(matchingDownload.version, currentVersion.version) > 0;
if (hasUpdate) {
setUpdateInfo({
currentVersion,
availableUpdate: matchingDownload,
});
setShowUpdateModal(true);
}
}
} catch (error) {
window.electronAPI.logs.logError(
'Failed to check for updates:',
error as Error
);
} finally {
setIsChecking(false);
}
}, []);
const dismissUpdate = useCallback(() => {
setShowUpdateModal(false);
setUpdateInfo(null);
}, []);
return {
updateInfo,
showUpdateModal,
isChecking,
checkForUpdates,
dismissUpdate,
};
};

View file

@ -10,6 +10,7 @@ import {
writeFileSync,
unlinkSync,
} from 'fs';
import { rm } from 'fs/promises';
import { dialog } from 'electron';
import { GitHubService } from '@/main/services/GitHubService';
import { ConfigManager } from '@/main/managers/ConfigManager';
@ -23,6 +24,8 @@ interface GitHubAsset {
browser_download_url: string;
size: number;
created_at: string;
isUpdate?: boolean;
wasCurrentBinary?: boolean;
}
interface GitHubRelease {
@ -85,6 +88,17 @@ export class KoboldCppManager {
const baseFilename = asset.name.replace(/\.exe$/, '');
const unpackedDirPath = join(this.installDir, baseFilename);
if (asset.isUpdate && existsSync(unpackedDirPath)) {
try {
await rm(unpackedDirPath, { recursive: true, force: true });
} catch (error) {
this.logManager.logError(
'Failed to remove existing directory for update:',
error as Error
);
}
}
const response = await fetch(asset.browser_download_url);
if (!response.ok) {
@ -150,7 +164,7 @@ export class KoboldCppManager {
const launcherPath = this.getLauncherPath(unpackedDirPath);
if (launcherPath && existsSync(launcherPath)) {
const currentBinary = this.configManager.getCurrentKoboldBinary();
if (!currentBinary) {
if (!currentBinary || (asset.isUpdate && asset.wasCurrentBinary)) {
this.configManager.setCurrentKoboldBinary(launcherPath);
}

View file

@ -8,6 +8,7 @@ import {
} from 'electron';
import * as os from 'os';
import { join } from 'path';
import { GITHUB_API } from '../../constants';
export class WindowManager {
private mainWindow: BrowserWindow | null = null;
@ -255,7 +256,9 @@ export class WindowManager {
{
label: 'KoboldCpp Wiki',
click: () => {
shell.openExternal('https://github.com/LostRuins/koboldcpp/wiki');
shell.openExternal(
`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/wiki`
);
},
},
{
@ -327,7 +330,9 @@ OS: ${osInfo}`;
});
ipcMain.once('open-github', () => {
shell.openExternal('https://github.com/lone-cloud/friendly-kobold');
shell.openExternal(
`https://github.com/${GITHUB_API.FRIENDLY_KOBOLD_REPO}`
);
});
ipcMain.once('close-about-dialog', () => {

View file

@ -11,6 +11,8 @@ interface GitHubAsset {
browser_download_url: string;
size: number;
created_at: string;
isUpdate?: boolean;
wasCurrentBinary?: boolean;
}
export interface GitHubRelease {

View file

@ -40,7 +40,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/core@npm:^7.27.7, @babel/core@npm:^7.28.0":
"@babel/core@npm:^7.27.7, @babel/core@npm:^7.28.3":
version: 7.28.3
resolution: "@babel/core@npm:7.28.3"
dependencies:
@ -1661,10 +1661,10 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/pluginutils@npm:1.0.0-beta.30":
version: 1.0.0-beta.30
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.30"
checksum: 10c0/aff8b532cb9d82d94c9a4101fa12ecb10620ad47d52dbb9135a5c65bde1ad19895b41026b821f4d607083699239a5d0010198401b6a6a54ab6a10d0015302768
"@rolldown/pluginutils@npm:1.0.0-beta.32":
version: 1.0.0-beta.32
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.32"
checksum: 10c0/ba3582fc3c35c8eb57b0df2d22d0733b1be83d37edcc258203364773f094f58fc0cb7a056d604603573a69dd0105a466506cad467f59074e1e53d0dc26191f06
languageName: node
linkType: hard
@ -2191,19 +2191,19 @@ __metadata:
languageName: node
linkType: hard
"@vitejs/plugin-react@npm:^5.0.0":
version: 5.0.0
resolution: "@vitejs/plugin-react@npm:5.0.0"
"@vitejs/plugin-react@npm:^5.0.1":
version: 5.0.1
resolution: "@vitejs/plugin-react@npm:5.0.1"
dependencies:
"@babel/core": "npm:^7.28.0"
"@babel/core": "npm:^7.28.3"
"@babel/plugin-transform-react-jsx-self": "npm:^7.27.1"
"@babel/plugin-transform-react-jsx-source": "npm:^7.27.1"
"@rolldown/pluginutils": "npm:1.0.0-beta.30"
"@rolldown/pluginutils": "npm:1.0.0-beta.32"
"@types/babel__core": "npm:^7.20.5"
react-refresh: "npm:^0.17.0"
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
checksum: 10c0/e5813839d319ab5dc1b90cab40b6c08388f26e456166ba9df10ffc3c3f4ecc594cec06715b5c93390bba56140ca5f68a18f2233f7d275d77e5bbfeb979e4fd9b
checksum: 10c0/2641171beedfc38edc5671abb47706906f9af2a79a6dfff4e946106c9550de4f83ccae41c164f3ee26a3edf07127ecc0e415fe5cddbf7abc71fbb2540016c27d
languageName: node
linkType: hard
@ -3615,16 +3615,16 @@ __metadata:
languageName: node
linkType: hard
"electron@npm:^37.3.0":
version: 37.3.0
resolution: "electron@npm:37.3.0"
"electron@npm:^37.3.1":
version: 37.3.1
resolution: "electron@npm:37.3.1"
dependencies:
"@electron/get": "npm:^2.0.0"
"@types/node": "npm:^22.7.7"
extract-zip: "npm:^2.0.1"
bin:
electron: cli.js
checksum: 10c0/6f08ca2955c65591368cb0dadbd637d86749040c8d4cd616d7265e1c71ece854f23c678b7cfc23746100327dcdb34f4815504745e97ca3ac2f40c72b951e1044
checksum: 10c0/1b171804646f229768c29413629b70b968c4c06686913b69e8615da09c58d63a95817021c31ed3c3bdbba0ad360ad4469773b9dda2662884d7df2866559f95f1
languageName: node
linkType: hard
@ -4304,7 +4304,7 @@ __metadata:
languageName: node
linkType: hard
"fdir@npm:^6.4.4, fdir@npm:^6.4.6":
"fdir@npm:^6.4.4, fdir@npm:^6.5.0":
version: 6.5.0
resolution: "fdir@npm:6.5.0"
peerDependencies:
@ -4433,10 +4433,10 @@ __metadata:
"@types/react-dom": "npm:^19.1.7"
"@typescript-eslint/eslint-plugin": "npm:^8.40.0"
"@typescript-eslint/parser": "npm:^8.40.0"
"@vitejs/plugin-react": "npm:^5.0.0"
"@vitejs/plugin-react": "npm:^5.0.1"
cross-env: "npm:^10.0.0"
cspell: "npm:^9.2.0"
electron: "npm:^37.3.0"
electron: "npm:^37.3.1"
electron-builder: "npm:^26.0.12"
electron-vite: "npm:^4.0.0"
eslint: "npm:^9.33.0"
@ -4457,7 +4457,7 @@ __metadata:
rollup-plugin-visualizer: "npm:^6.0.3"
systeminformation: "npm:^5.27.7"
typescript: "npm:^5.9.2"
vite: "npm:^7.1.2"
vite: "npm:^7.1.3"
wait-on: "npm:^8.0.4"
languageName: unknown
linkType: soft
@ -8180,12 +8180,12 @@ __metadata:
languageName: node
linkType: hard
"vite@npm:^7.1.2":
version: 7.1.2
resolution: "vite@npm:7.1.2"
"vite@npm:^7.1.3":
version: 7.1.3
resolution: "vite@npm:7.1.3"
dependencies:
esbuild: "npm:^0.25.0"
fdir: "npm:^6.4.6"
fdir: "npm:^6.5.0"
fsevents: "npm:~2.3.3"
picomatch: "npm:^4.0.3"
postcss: "npm:^8.5.6"
@ -8231,7 +8231,7 @@ __metadata:
optional: true
bin:
vite: bin/vite.js
checksum: 10c0/4ed825b20bc0f49db99cd382de9506b2721ccd47dcebd4a68e0ef65e3cdd2347fded52b306c34178308e0fd7fe78fd5ff517623002cb00710182ad3012c92ced
checksum: 10c0/a0aa418beab80673dc9a3e9d1fa49472955d6ef9d41a4c9c6bd402953f411346f612864dae267adfb2bb8ceeb894482369316ffae5816c84fd45990e352b727d
languageName: node
linkType: hard