mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 09:33:10 -07:00
force getCurrentVersion usage instead of direct config reading, better about modal
This commit is contained in:
parent
01c64049fb
commit
a1cc5571c8
12 changed files with 222 additions and 384 deletions
|
|
@ -125,6 +125,18 @@ const config = [
|
|||
message:
|
||||
'Synchronous file operations are forbidden. Use async alternatives.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
'CallExpression[callee.object.object.object.name="window"][callee.object.object.property.name="electronAPI"][callee.object.property.name="config"][callee.property.name="get"] Literal[value="currentKoboldBinary"]',
|
||||
message:
|
||||
'Direct access to currentKoboldBinary config is forbidden. Use window.electronAPI.kobold.getCurrentVersion() instead to get proper fallback logic.',
|
||||
},
|
||||
{
|
||||
selector:
|
||||
'CallExpression[callee.object.object.object.name="window"][callee.object.object.property.name="electronAPI"][callee.object.property.name="config"][callee.property.name="set"] Literal[value="currentKoboldBinary"]',
|
||||
message:
|
||||
'Direct setting of currentKoboldBinary config is forbidden. Use window.electronAPI.kobold.setCurrentVersion() instead.',
|
||||
},
|
||||
],
|
||||
|
||||
'import/no-default-export': 'error',
|
||||
|
|
|
|||
46
src/App.tsx
46
src/App.tsx
|
|
@ -47,12 +47,8 @@ export const App = () => {
|
|||
useEffect(() => {
|
||||
const checkInstallation = async () => {
|
||||
await Logger.safeExecute(async () => {
|
||||
const [versions, currentBinaryPath, hasSeenWelcome, preference] =
|
||||
await Promise.all([
|
||||
window.electronAPI.kobold.getInstalledVersions(),
|
||||
window.electronAPI.config.get(
|
||||
'currentKoboldBinary'
|
||||
) as Promise<string>,
|
||||
const [currentVersion, hasSeenWelcome, preference] = await Promise.all([
|
||||
window.electronAPI.kobold.getCurrentVersion(),
|
||||
window.electronAPI.config.get('hasSeenWelcome') as Promise<boolean>,
|
||||
window.electronAPI.config.get(
|
||||
'frontendPreference'
|
||||
|
|
@ -63,26 +59,13 @@ export const App = () => {
|
|||
|
||||
if (!hasSeenWelcome) {
|
||||
setCurrentScreenWithTransition('welcome');
|
||||
} else if (versions.length > 0) {
|
||||
let current = null;
|
||||
if (currentBinaryPath) {
|
||||
current = versions.find((v) => v.path === currentBinaryPath);
|
||||
}
|
||||
if (!current) {
|
||||
current = versions[0];
|
||||
if (current) {
|
||||
await window.electronAPI.config.set(
|
||||
'currentKoboldBinary',
|
||||
current.path
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (currentVersion) {
|
||||
setCurrentScreenWithTransition('launch');
|
||||
} else {
|
||||
setCurrentScreenWithTransition('download');
|
||||
}
|
||||
|
||||
if (versions.length > 0) {
|
||||
if (currentVersion) {
|
||||
setTimeout(() => {
|
||||
checkForUpdates();
|
||||
}, 2000);
|
||||
|
|
@ -112,26 +95,7 @@ export const App = () => {
|
|||
|
||||
const handleDownloadComplete = async () => {
|
||||
await Logger.safeExecute(async () => {
|
||||
const [versions, currentBinaryPath] = await Promise.all([
|
||||
window.electronAPI.kobold.getInstalledVersions(),
|
||||
window.electronAPI.config.get('currentKoboldBinary') as Promise<string>,
|
||||
]);
|
||||
|
||||
if (versions.length > 0) {
|
||||
let current = null;
|
||||
if (currentBinaryPath) {
|
||||
current = versions.find((v) => v.path === currentBinaryPath);
|
||||
}
|
||||
if (!current) {
|
||||
current = versions[0];
|
||||
if (current) {
|
||||
await window.electronAPI.config.set(
|
||||
'currentKoboldBinary',
|
||||
current.path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await window.electronAPI.kobold.getCurrentVersion();
|
||||
}, 'Error refreshing versions after download:');
|
||||
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -64,38 +64,13 @@ export const VersionsTab = () => {
|
|||
setLoadingInstalled(true);
|
||||
|
||||
await Logger.safeExecute(async () => {
|
||||
const [versions, currentBinaryPath] = await Promise.all([
|
||||
const [versions, currentVersion] = await Promise.all([
|
||||
window.electronAPI.kobold.getInstalledVersions(),
|
||||
window.electronAPI.config.get('currentKoboldBinary') as Promise<string>,
|
||||
window.electronAPI.kobold.getCurrentVersion(),
|
||||
]);
|
||||
|
||||
setInstalledVersions(versions);
|
||||
|
||||
if (currentBinaryPath && versions.length > 0) {
|
||||
const current = versions.find((v) => v.path === currentBinaryPath);
|
||||
|
||||
if (current) {
|
||||
setCurrentVersion(current);
|
||||
} else {
|
||||
setCurrentVersion(versions[0]);
|
||||
await window.electronAPI.config.set(
|
||||
'currentKoboldBinary',
|
||||
versions[0].path
|
||||
);
|
||||
}
|
||||
} else if (versions.length > 0) {
|
||||
setCurrentVersion(versions[0]);
|
||||
|
||||
await window.electronAPI.config.set(
|
||||
'currentKoboldBinary',
|
||||
versions[0].path
|
||||
);
|
||||
} else {
|
||||
setCurrentVersion(null);
|
||||
if (currentBinaryPath) {
|
||||
await window.electronAPI.config.set('currentKoboldBinary', '');
|
||||
}
|
||||
}
|
||||
setCurrentVersion(currentVersion);
|
||||
}, 'Failed to load installed versions:');
|
||||
|
||||
setLoadingInstalled(false);
|
||||
|
|
@ -245,17 +220,7 @@ export const VersionsTab = () => {
|
|||
);
|
||||
|
||||
if (success) {
|
||||
await window.electronAPI.config.set(
|
||||
'currentKoboldBinary',
|
||||
version.installedPath!
|
||||
);
|
||||
|
||||
const newCurrentVersion = installedVersions.find(
|
||||
(v) => v.path === version.installedPath
|
||||
);
|
||||
if (newCurrentVersion) {
|
||||
setCurrentVersion(newCurrentVersion);
|
||||
}
|
||||
await loadInstalledVersions();
|
||||
}
|
||||
}, 'Failed to set current version:');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { getDisplayNameFromPath, compareVersions } from '@/utils/version';
|
||||
import {
|
||||
getDisplayNameFromPath,
|
||||
compareVersions,
|
||||
stripAssetExtensions,
|
||||
} from '@/utils/version';
|
||||
import { useKoboldVersions } from '@/hooks/useKoboldVersions';
|
||||
import { getROCmDownload } from '@/utils/rocm';
|
||||
import type { InstalledVersion, DownloadItem } from '@/types/electron';
|
||||
|
|
@ -64,22 +68,14 @@ export const useUpdateChecker = () => {
|
|||
setIsChecking(true);
|
||||
|
||||
try {
|
||||
const [currentBinaryPath, installedVersionsResult, rocmDownload] =
|
||||
await Promise.all([
|
||||
window.electronAPI.config.get(
|
||||
'currentKoboldBinary'
|
||||
) as Promise<string>,
|
||||
window.electronAPI.kobold.getInstalledVersions(),
|
||||
const [currentVersion, rocmDownload] = await Promise.all([
|
||||
window.electronAPI.kobold.getCurrentVersion(),
|
||||
getROCmDownload(),
|
||||
]);
|
||||
|
||||
if (!currentBinaryPath || installedVersionsResult.length === 0) {
|
||||
if (!currentVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVersion = installedVersionsResult.find(
|
||||
(v: InstalledVersion) => v.path === currentBinaryPath
|
||||
);
|
||||
if (!currentVersion) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -93,9 +89,7 @@ export const useUpdateChecker = () => {
|
|||
|
||||
const matchingDownload = availableDownloads.find(
|
||||
(download: DownloadItem) => {
|
||||
const downloadBaseName = download.name
|
||||
.replace(/\.(tar\.gz|zip|exe)$/i, '')
|
||||
.replace(/\.packed$/, '');
|
||||
const downloadBaseName = stripAssetExtensions(download.name);
|
||||
return downloadBaseName === currentDisplayName;
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ export class IPCHandlers {
|
|||
this.koboldManager.getInstalledVersions()
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:getCurrentVersion', () =>
|
||||
this.koboldManager.getCurrentVersion()
|
||||
);
|
||||
|
||||
ipcMain.handle('kobold:getConfigFiles', () =>
|
||||
this.koboldManager.getConfigFiles()
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { rm, readdir, stat } from 'fs/promises';
|
||||
import { rm, readdir, stat, unlink, rename, mkdir, chmod } from 'fs/promises';
|
||||
import { dialog } from 'electron';
|
||||
import axios from 'axios';
|
||||
|
||||
import { execa } from 'execa';
|
||||
import { terminateProcess } from '@/utils/process';
|
||||
|
|
@ -9,8 +11,8 @@ import { ConfigManager } from '@/main/managers/ConfigManager';
|
|||
import { LogManager } from '@/main/managers/LogManager';
|
||||
import { WindowManager } from '@/main/managers/WindowManager';
|
||||
import { PRODUCT_NAME, SERVER_READY_SIGNALS } from '@/constants';
|
||||
import { downloadAndUnpackBinary } from '@/utils/server/download';
|
||||
import { pathExists, readJsonFile, writeJsonFile } from '@/utils/fs';
|
||||
import { stripAssetExtensions } from '@/utils/version';
|
||||
import type {
|
||||
GitHubAsset,
|
||||
InstalledVersion,
|
||||
|
|
@ -68,40 +70,165 @@ export class KoboldCppManager {
|
|||
}
|
||||
}
|
||||
|
||||
async downloadRelease(asset: GitHubAsset): Promise<string> {
|
||||
const result = await downloadAndUnpackBinary(
|
||||
{
|
||||
name: asset.name,
|
||||
private async handleExistingDirectory(
|
||||
unpackedDirPath: string,
|
||||
isUpdate: boolean
|
||||
): Promise<void> {
|
||||
if (!isUpdate || !(await pathExists(unpackedDirPath))) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.koboldProcess && !this.koboldProcess.killed) {
|
||||
this.windowManager.sendKoboldOutput(
|
||||
'Stopping KoboldCpp process before update...'
|
||||
);
|
||||
await this.cleanup();
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
await this.removeDirectoryWithRetry(unpackedDirPath);
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to remove existing directory for update:',
|
||||
error as Error
|
||||
);
|
||||
throw new Error(
|
||||
`Cannot update: Failed to remove existing installation. ` +
|
||||
`Please ensure KoboldCpp is stopped and try again. ` +
|
||||
`Error: ${(error as Error).message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadFile(
|
||||
asset: GitHubAsset,
|
||||
tempPackedFilePath: string
|
||||
): Promise<void> {
|
||||
const writer = createWriteStream(tempPackedFilePath);
|
||||
let downloadedBytes = 0;
|
||||
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: asset.browser_download_url,
|
||||
size: asset.size,
|
||||
version: asset.version,
|
||||
},
|
||||
{
|
||||
installDir: this.installDir,
|
||||
onProgress: (progress: number) => {
|
||||
responseType: 'stream',
|
||||
timeout: 30000,
|
||||
maxRedirects: 5,
|
||||
});
|
||||
|
||||
const totalBytes = asset.size;
|
||||
|
||||
response.data.on('data', (chunk: Buffer) => {
|
||||
downloadedBytes += chunk.length;
|
||||
if (totalBytes > 0) {
|
||||
const progress = (downloadedBytes / totalBytes) * 100;
|
||||
const mainWindow = this.windowManager.getMainWindow();
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', progress);
|
||||
}
|
||||
},
|
||||
isUpdate: asset.isUpdate,
|
||||
wasCurrentBinary: asset.wasCurrentBinary,
|
||||
logManager: this.logManager,
|
||||
configManager: this.configManager,
|
||||
windowManager: this.windowManager,
|
||||
unpackFunction: this.unpackKoboldCpp.bind(this),
|
||||
getLauncherPath: this.getLauncherPath.bind(this),
|
||||
removeDirectoryWithRetry: this.removeDirectoryWithRetry.bind(this),
|
||||
cleanup: this.cleanup.bind(this),
|
||||
koboldProcess: this.koboldProcess || undefined,
|
||||
}
|
||||
});
|
||||
|
||||
response.data.pipe(writer);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
writer.on('finish', async () => {
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
await chmod(tempPackedFilePath, 0o755);
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to make binary executable:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
writer.on('error', reject);
|
||||
response.data.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
private async setupLauncher(
|
||||
tempPackedFilePath: string,
|
||||
unpackedDirPath: string
|
||||
): Promise<string> {
|
||||
let launcherPath = await this.getLauncherPath(unpackedDirPath);
|
||||
|
||||
if (!launcherPath || !(await pathExists(launcherPath))) {
|
||||
const expectedLauncherName =
|
||||
process.platform === 'win32'
|
||||
? 'koboldcpp-launcher.exe'
|
||||
: 'koboldcpp-launcher';
|
||||
const newLauncherPath = join(unpackedDirPath, expectedLauncherName);
|
||||
|
||||
if (await pathExists(tempPackedFilePath)) {
|
||||
try {
|
||||
await rename(tempPackedFilePath, newLauncherPath);
|
||||
launcherPath = newLauncherPath;
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to rename binary as launcher:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await unlink(tempPackedFilePath);
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to cleanup packed file:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!launcherPath || !(await pathExists(launcherPath))) {
|
||||
throw new Error('Failed to find or create koboldcpp launcher');
|
||||
}
|
||||
|
||||
return launcherPath;
|
||||
}
|
||||
|
||||
async downloadRelease(asset: GitHubAsset): Promise<string> {
|
||||
const tempPackedFilePath = join(this.installDir, `${asset.name}.packed`);
|
||||
const baseFilename = stripAssetExtensions(asset.name);
|
||||
const folderName = asset.version
|
||||
? `${baseFilename}-${asset.version}`
|
||||
: baseFilename;
|
||||
const unpackedDirPath = join(this.installDir, folderName);
|
||||
|
||||
try {
|
||||
await this.handleExistingDirectory(
|
||||
unpackedDirPath,
|
||||
Boolean(asset.isUpdate)
|
||||
);
|
||||
await this.downloadFile(asset, tempPackedFilePath);
|
||||
await mkdir(unpackedDirPath, { recursive: true });
|
||||
await this.unpackKoboldCpp(tempPackedFilePath, unpackedDirPath);
|
||||
const launcherPath = await this.setupLauncher(
|
||||
tempPackedFilePath,
|
||||
unpackedDirPath
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Download failed');
|
||||
const currentBinary = this.configManager.getCurrentKoboldBinary();
|
||||
if (!currentBinary || (asset.isUpdate && asset.wasCurrentBinary)) {
|
||||
await this.configManager.setCurrentKoboldBinary(launcherPath);
|
||||
}
|
||||
|
||||
return result.path!;
|
||||
this.windowManager.sendToRenderer('versions-updated');
|
||||
return launcherPath;
|
||||
} catch (error) {
|
||||
this.logManager.logError(
|
||||
'Failed to download or unpack binary:',
|
||||
error as Error
|
||||
);
|
||||
throw new Error(
|
||||
`Failed to download or unpack binary: ${(error as Error).message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async unpackKoboldCpp(
|
||||
|
|
@ -362,7 +489,9 @@ export class KoboldCppManager {
|
|||
|
||||
const folderName = launcherPath.split(/[/\\]/).slice(-2, -1)[0];
|
||||
if (folderName) {
|
||||
const versionMatch = folderName.match(/-(\d+\.\d+(?:\.\d+)?)$/);
|
||||
const versionMatch = folderName.match(
|
||||
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/
|
||||
);
|
||||
if (versionMatch) {
|
||||
return versionMatch[1];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -316,9 +316,13 @@ Node.js: ${nodeVersion}
|
|||
V8: ${v8Version}
|
||||
OS: ${osInfo}`;
|
||||
|
||||
const iconPath = this.getIconPath();
|
||||
const iconImage = nativeImage.createFromPath(iconPath);
|
||||
|
||||
const aboutWindow = new BrowserWindow({
|
||||
width: 500,
|
||||
height: 400,
|
||||
width: 400,
|
||||
height: 450,
|
||||
icon: iconImage,
|
||||
modal: true,
|
||||
parent: this.mainWindow!,
|
||||
resizable: false,
|
||||
|
|
@ -334,7 +338,7 @@ OS: ${osInfo}`;
|
|||
|
||||
aboutWindow.setMenu(null);
|
||||
|
||||
const htmlPath = this.getTemplatePath('about-dialog.html');
|
||||
const htmlPath = this.getTemplatePath('about-modal.html');
|
||||
await aboutWindow.loadFile(htmlPath);
|
||||
|
||||
aboutWindow.webContents.executeJavaScript(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
|
||||
const koboldAPI: KoboldAPI = {
|
||||
getInstalledVersions: () => ipcRenderer.invoke('kobold:getInstalledVersions'),
|
||||
getCurrentVersion: () => ipcRenderer.invoke('kobold:getCurrentVersion'),
|
||||
setCurrentVersion: (version: string) =>
|
||||
ipcRenderer.invoke('kobold:setCurrentVersion', version),
|
||||
getPlatform: () => ipcRenderer.invoke('kobold:getPlatform'),
|
||||
|
|
|
|||
1
src/types/electron.d.ts
vendored
1
src/types/electron.d.ts
vendored
|
|
@ -95,6 +95,7 @@ export interface KoboldConfig {
|
|||
|
||||
export interface KoboldAPI {
|
||||
getInstalledVersions: () => Promise<InstalledVersion[]>;
|
||||
getCurrentVersion: () => Promise<InstalledVersion | null>;
|
||||
setCurrentVersion: (version: string) => Promise<boolean>;
|
||||
getPlatform: () => Promise<string>;
|
||||
detectGPU: () => Promise<BasicGPUInfo>;
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
import { createWriteStream } from 'fs';
|
||||
import { unlink, rename, mkdir, chmod } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import axios from 'axios';
|
||||
import type { LogManager } from '@/main/managers/LogManager';
|
||||
import type { ConfigManager } from '@/main/managers/ConfigManager';
|
||||
import type { WindowManager } from '@/main/managers/WindowManager';
|
||||
import type { DownloadItem } from '@/types/electron';
|
||||
import { stripAssetExtensions } from '@/utils/version';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
import { pathExists } from '@/utils/fs';
|
||||
|
||||
export interface DownloadOptions {
|
||||
installDir: string;
|
||||
onProgress?: (progress: number) => void;
|
||||
isUpdate?: boolean;
|
||||
wasCurrentBinary?: boolean;
|
||||
version?: string;
|
||||
logManager: LogManager;
|
||||
configManager: ConfigManager;
|
||||
windowManager: WindowManager;
|
||||
unpackFunction: (packedPath: string, unpackDir: string) => Promise<void>;
|
||||
getLauncherPath: (unpackedDir: string) => Promise<string | null>;
|
||||
removeDirectoryWithRetry?: (dirPath: string) => Promise<void>;
|
||||
cleanup?: () => Promise<void>;
|
||||
koboldProcess?: ChildProcess;
|
||||
}
|
||||
|
||||
export interface DownloadResult {
|
||||
success: boolean;
|
||||
path?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
async function handleExistingDirectory(
|
||||
unpackedDirPath: string,
|
||||
isUpdate: boolean,
|
||||
koboldProcess: ChildProcess | undefined,
|
||||
windowManager: WindowManager,
|
||||
cleanup: (() => Promise<void>) | undefined,
|
||||
removeDirectoryWithRetry: ((dirPath: string) => Promise<void>) | undefined,
|
||||
logManager: LogManager
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
if (!isUpdate || !(await pathExists(unpackedDirPath))) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
try {
|
||||
if (koboldProcess && !koboldProcess.killed) {
|
||||
windowManager.sendKoboldOutput(
|
||||
'Stopping KoboldCpp process before update...'
|
||||
);
|
||||
|
||||
if (cleanup) {
|
||||
await cleanup();
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
|
||||
if (removeDirectoryWithRetry) {
|
||||
await removeDirectoryWithRetry(unpackedDirPath);
|
||||
}
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logManager.logError(
|
||||
'Failed to remove existing directory for update:',
|
||||
error as Error
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
`Cannot update: Failed to remove existing installation. ` +
|
||||
`Please ensure KoboldCpp is stopped and try again. ` +
|
||||
`Error: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(
|
||||
item: DownloadItem,
|
||||
tempPackedFilePath: string,
|
||||
onProgress: ((progress: number) => void) | undefined,
|
||||
logManager: LogManager
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
const writer = createWriteStream(tempPackedFilePath);
|
||||
let downloadedBytes = 0;
|
||||
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: item.url,
|
||||
responseType: 'stream',
|
||||
timeout: 30000,
|
||||
maxRedirects: 5,
|
||||
});
|
||||
|
||||
const totalBytes = item.size;
|
||||
|
||||
response.data.on('data', (chunk: Buffer) => {
|
||||
downloadedBytes += chunk.length;
|
||||
if (onProgress && totalBytes > 0) {
|
||||
onProgress((downloadedBytes / totalBytes) * 100);
|
||||
}
|
||||
});
|
||||
|
||||
response.data.pipe(writer);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', async () => {
|
||||
if (process.platform !== 'win32') {
|
||||
try {
|
||||
await chmod(tempPackedFilePath, 0o755);
|
||||
} catch (error) {
|
||||
logManager.logError(
|
||||
'Failed to make binary executable:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
resolve({ success: true });
|
||||
});
|
||||
writer.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
response.data.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
return { success: false, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
export async function downloadAndUnpackBinary(
|
||||
item: DownloadItem,
|
||||
options: DownloadOptions
|
||||
): Promise<DownloadResult> {
|
||||
const {
|
||||
installDir,
|
||||
onProgress,
|
||||
isUpdate,
|
||||
wasCurrentBinary,
|
||||
version,
|
||||
logManager,
|
||||
configManager,
|
||||
windowManager,
|
||||
unpackFunction,
|
||||
getLauncherPath,
|
||||
removeDirectoryWithRetry,
|
||||
cleanup,
|
||||
koboldProcess,
|
||||
} = options;
|
||||
|
||||
const tempPackedFilePath = join(installDir, `${item.name}.packed`);
|
||||
const baseFilename = stripAssetExtensions(item.name);
|
||||
const folderName = version ? `${baseFilename}-${version}` : baseFilename;
|
||||
const unpackedDirPath = join(installDir, folderName);
|
||||
|
||||
const dirResult = await handleExistingDirectory(
|
||||
unpackedDirPath,
|
||||
Boolean(isUpdate),
|
||||
koboldProcess,
|
||||
windowManager,
|
||||
cleanup,
|
||||
removeDirectoryWithRetry,
|
||||
logManager
|
||||
);
|
||||
|
||||
if (!dirResult.success) {
|
||||
return { success: false, error: dirResult.error };
|
||||
}
|
||||
|
||||
const downloadResult = await downloadFile(
|
||||
item,
|
||||
tempPackedFilePath,
|
||||
onProgress,
|
||||
logManager
|
||||
);
|
||||
if (!downloadResult.success) {
|
||||
return { success: false, error: downloadResult.error };
|
||||
}
|
||||
|
||||
try {
|
||||
await mkdir(unpackedDirPath, { recursive: true });
|
||||
await unpackFunction(tempPackedFilePath, unpackedDirPath);
|
||||
|
||||
let launcherPath = await getLauncherPath(unpackedDirPath);
|
||||
|
||||
if (!launcherPath || !(await pathExists(launcherPath))) {
|
||||
const expectedLauncherName =
|
||||
process.platform === 'win32'
|
||||
? 'koboldcpp-launcher.exe'
|
||||
: 'koboldcpp-launcher';
|
||||
const newLauncherPath = join(unpackedDirPath, expectedLauncherName);
|
||||
|
||||
if (await pathExists(tempPackedFilePath)) {
|
||||
try {
|
||||
await rename(tempPackedFilePath, newLauncherPath);
|
||||
launcherPath = newLauncherPath;
|
||||
} catch (error) {
|
||||
logManager.logError(
|
||||
'Failed to rename binary as launcher:',
|
||||
error as Error
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await unlink(tempPackedFilePath);
|
||||
} catch (error) {
|
||||
logManager.logError('Failed to cleanup packed file:', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
if (launcherPath && (await pathExists(launcherPath))) {
|
||||
const currentBinary = configManager.getCurrentKoboldBinary();
|
||||
if (!currentBinary || (isUpdate && wasCurrentBinary)) {
|
||||
await configManager.setCurrentKoboldBinary(launcherPath);
|
||||
}
|
||||
|
||||
windowManager.sendToRenderer('versions-updated');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: launcherPath,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to find or create koboldcpp launcher',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
logManager.logError('Failed to unpack binary:', error as Error);
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to unpack binary: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -9,16 +9,20 @@ export const getDisplayNameFromPath = (
|
|||
);
|
||||
|
||||
if (launcherIndex > 0) {
|
||||
return pathParts[launcherIndex - 1];
|
||||
return stripVersionSuffix(pathParts[launcherIndex - 1]);
|
||||
}
|
||||
|
||||
return installedVersion.filename;
|
||||
};
|
||||
|
||||
export const stripAssetExtensions = (assetName: string): string =>
|
||||
assetName
|
||||
.replace(/\.(tar\.gz|zip|exe|dmg|AppImage)$/i, '')
|
||||
.replace(/\.packed$/, '');
|
||||
assetName.replace(/\.(tar\.gz|zip|exe|dmg|AppImage)$/i, '');
|
||||
|
||||
const stripVersionSuffix = (displayName: string): string =>
|
||||
displayName.replace(
|
||||
/-(\d+\.\d+(?:\.\d+)?(?:\.[a-zA-Z0-9]+)*(?:-[a-zA-Z0-9]+)*)$/,
|
||||
''
|
||||
);
|
||||
|
||||
export const compareVersions = (versionA: string, versionB: string): number => {
|
||||
const cleanVersion = (version: string): string =>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue