more sillytavern integration improvements, default configs to .json

This commit is contained in:
lone-cloud 2025-08-29 03:14:14 -07:00
parent 07ebcb182e
commit ae8ec47f51
25 changed files with 235 additions and 647 deletions

View file

@ -83,7 +83,6 @@
"@mantine/core": "^8.2.7", "@mantine/core": "^8.2.7",
"@mantine/hooks": "^8.2.7", "@mantine/hooks": "^8.2.7",
"execa": "^9.6.0", "execa": "^9.6.0",
"fkill": "^9.0.0",
"got": "^14.4.7", "got": "^14.4.7",
"lucide-react": "^0.542.0", "lucide-react": "^0.542.0",
"react": "^19.1.1", "react": "^19.1.1",

View file

@ -12,7 +12,7 @@ import {
import { Download, X, ExternalLink } from 'lucide-react'; import { Download, X, ExternalLink } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import type { InstalledVersion, DownloadItem } from '@/types/electron'; import type { InstalledVersion, DownloadItem } from '@/types/electron';
import { getDisplayNameFromPath } from '@/utils/versionUtils'; import { getDisplayNameFromPath } from '@/utils/version';
import { GITHUB_API } from '@/constants'; import { GITHUB_API } from '@/constants';
interface UpdateAvailableModalProps { interface UpdateAvailableModalProps {

View file

@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useRef } from 'react';
import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core'; import { Card, Text, Title, Loader, Stack, Container } from '@mantine/core';
import { DownloadCard } from '@/components/DownloadCard'; import { DownloadCard } from '@/components/DownloadCard';
import { getPlatformDisplayName } from '@/utils/platform'; import { getPlatformDisplayName } from '@/utils/platform';
import { formatDownloadSize } from '@/utils/downloadUtils'; import { formatDownloadSize } from '@/utils/download';
import { getAssetDescription, sortDownloadsByType } from '@/utils/assets'; import { getAssetDescription, sortDownloadsByType } from '@/utils/assets';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { DownloadItem } from '@/types/electron'; import type { DownloadItem } from '@/types/electron';

View file

@ -9,8 +9,7 @@ import {
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import styles from '@/styles/layout.module.css'; import styles from '@/styles/layout.module.css';
import { UI } from '@/constants'; import { UI } from '@/constants';
import { handleTerminalOutput } from '@/utils/terminal'; import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal';
import { processTerminalContent } from '@/utils/linkifyTerminal';
import { useLaunchConfigStore } from '@/stores/launchConfigStore'; import { useLaunchConfigStore } from '@/stores/launchConfigStore';
import type { FrontendPreference } from '@/types'; import type { FrontendPreference } from '@/types';

View file

@ -1,4 +1,4 @@
import { Stack, Text, Group, Button, Select, Badge } from '@mantine/core'; import { Stack, Text, Group, Button, Select } from '@mantine/core';
import { useState, useCallback } from 'react'; import { useState, useCallback } from 'react';
import { Save, File, Plus, Check } from 'lucide-react'; import { Save, File, Plus, Check } from 'lucide-react';
import type { ConfigFile } from '@/types'; import type { ConfigFile } from '@/types';
@ -14,33 +14,6 @@ interface ConfigFileManagerProps {
onLoadConfigFiles: () => Promise<void>; onLoadConfigFiles: () => Promise<void>;
} }
interface SelectItemProps {
label: string;
extension: string;
}
const getBadgeColor = (extension: string) => {
switch (extension.toLowerCase()) {
case '.kcpps':
return 'blue';
case '.kcppt':
return 'green';
default:
return 'gray';
}
};
const SelectItem = ({ label, extension }: SelectItemProps) => (
<Group justify="space-between" wrap="nowrap">
<Text size="sm" truncate>
{label}
</Text>
<Badge size="xs" variant="light" color={getBadgeColor(extension)}>
{extension}
</Badge>
</Group>
);
export const ConfigFileManager = ({ export const ConfigFileManager = ({
configFiles, configFiles,
selectedFile, selectedFile,
@ -110,15 +83,6 @@ export const ConfigFileManager = ({
leftSection={<File size={16} />} leftSection={<File size={16} />}
searchable searchable
clearable={false} clearable={false}
renderOption={({ option }) => {
const dataItem = selectData.find(
(item) => item.value === option.value
);
const extension = dataItem?.extension || '';
return (
<SelectItem label={option.label} extension={extension} />
);
}}
/> />
</div> </div>
<Button <Button

View file

@ -195,16 +195,17 @@ export const LaunchScreen = ({
const handleCreateNewConfig = async (configName: string) => { const handleCreateNewConfig = async (configName: string) => {
try { try {
const fullConfigName = `${configName}.json`;
const success = await window.electronAPI.kobold.saveConfigFile( const success = await window.electronAPI.kobold.saveConfigFile(
configName, fullConfigName,
buildConfigData() buildConfigData()
); );
if (success) { if (success) {
await loadConfigFiles(); await loadConfigFiles();
const newFileName = `${configName}.kcpps`;
setSelectedFile(newFileName); setSelectedFile(fullConfigName);
await window.electronAPI.kobold.setSelectedConfig(newFileName); await window.electronAPI.kobold.setSelectedConfig(fullConfigName);
} else { } else {
window.electronAPI.logs.logError( window.electronAPI.logs.logError(
'Failed to create new configuration', 'Failed to create new configuration',
@ -229,10 +230,8 @@ export const LaunchScreen = ({
} }
try { try {
const configName = selectedFile.replace(/\.(kcpps|kcppt)$/, '');
const success = await window.electronAPI.kobold.saveConfigFile( const success = await window.electronAPI.kobold.saveConfigFile(
configName, selectedFile,
buildConfigData() buildConfigData()
); );

View file

@ -16,8 +16,10 @@ import { getAssetDescription, sortDownloadsByType } from '@/utils/assets';
import { import {
getDisplayNameFromPath, getDisplayNameFromPath,
stripAssetExtensions, stripAssetExtensions,
} from '@/utils/versionUtils'; compareVersions,
import { formatDownloadSize, compareVersions } from '@/utils/downloadUtils'; } from '@/utils/version';
import { formatDownloadSize } from '@/utils/download';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron'; import type { InstalledVersion, ReleaseWithStatus } from '@/types/electron';

View file

@ -1,5 +1,5 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { compareVersions } from '@/utils/downloadUtils'; import { compareVersions } from '@/utils/version';
import { GITHUB_API } from '@/constants'; import { GITHUB_API } from '@/constants';
interface AppUpdateInfo { interface AppUpdateInfo {

View file

@ -1,5 +1,6 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { GITHUB_API, ROCM } from '@/constants'; import { getROCmDownload } from '@/utils/rocm';
import { GITHUB_API } from '@/constants';
import { filterAssetsByPlatform } from '@/utils/platform'; import { filterAssetsByPlatform } from '@/utils/platform';
import type { import type {
DownloadItem, DownloadItem,
@ -91,44 +92,6 @@ const fetchLatestReleaseFromAPI = async (
return transformReleaseToDownloadItems(release, platform); return transformReleaseToDownloadItems(release, platform);
}; };
const getROCmDownload = async (
platform: string
): Promise<DownloadItem | null> => {
if (platform !== 'linux') {
return null;
}
try {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const latestRelease = await response.json();
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version,
type: 'rocm',
};
} catch (error) {
window.electronAPI.logs.logError(
'Failed to fetch ROCm version info:',
error as Error
);
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version: 'unknown',
type: 'rocm',
};
}
};
const getLatestReleaseWithDownloadStatus = const getLatestReleaseWithDownloadStatus =
async (): Promise<ReleaseWithStatus | null> => { async (): Promise<ReleaseWithStatus | null> => {
try { try {

View file

@ -1,6 +1,5 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback, useEffect } from 'react';
import { getDisplayNameFromPath } from '@/utils/versionUtils'; import { getDisplayNameFromPath, compareVersions } from '@/utils/version';
import { compareVersions } from '@/utils/downloadUtils';
import { useKoboldVersions } from '@/hooks/useKoboldVersions'; import { useKoboldVersions } from '@/hooks/useKoboldVersions';
import type { InstalledVersion, DownloadItem } from '@/types/electron'; import type { InstalledVersion, DownloadItem } from '@/types/electron';

View file

@ -2,9 +2,11 @@
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { existsSync, readFileSync } from 'fs'; import { existsSync, readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants';
import { homedir } from 'os'; import { homedir } from 'os';
import { PRODUCT_NAME, CONFIG_FILE_NAME } from '@/constants';
import { terminateProcess } from '@/utils/process';
export class LightweightCliHandler { export class LightweightCliHandler {
private getConfigDir(appName: string): string { private getConfigDir(appName: string): string {
const platform = process.platform; const platform = process.platform;
@ -95,16 +97,15 @@ export class LightweightCliHandler {
reject(error); reject(error);
}); });
const handleSignal = () => { const handleSignal = async () => {
console.log('\nReceived termination signal, terminating KoboldCpp...'); console.log('\nReceived termination signal, terminating KoboldCpp...');
if (!child.killed) { if (!child.killed) {
child.kill('SIGTERM'); await terminateProcess(child, {
timeoutMs: 5000,
setTimeout(() => { logError: (message, error) => {
if (!child.killed) { console.error(`${message} ${error.message}`);
child.kill('SIGKILL'); },
} });
}, 5000);
} }
}; };

View file

@ -30,22 +30,16 @@ export class IPCHandlers {
this.binaryService = binaryService; this.binaryService = binaryService;
} }
private async launchKoboldCppWithCustomFrontends( private async launchKoboldCppWithCustomFrontends(args: string[] = []) {
args: string[] = []
): Promise<{
success: boolean;
pid?: number;
error?: string;
}> {
try { try {
const frontendPreference = const frontendPreference =
await this.configManager.get('frontendPreference'); await this.configManager.get('frontendPreference');
if (frontendPreference === 'sillytavern') { this.koboldManager.launchKoboldCpp(args);
await this.sillyTavernManager.startFrontend(args);
}
return await this.koboldManager.launchKoboldCpp(args); if (frontendPreference === 'sillytavern') {
this.sillyTavernManager.startFrontend(args);
}
} catch (error) { } catch (error) {
this.logManager.logError('Error in enhanced launch:', error as Error); this.logManager.logError('Error in enhanced launch:', error as Error);
return { return {
@ -164,11 +158,10 @@ export class IPCHandlers {
ipcMain.handle('kobold:stopKoboldCpp', async () => { ipcMain.handle('kobold:stopKoboldCpp', async () => {
try { try {
this.koboldManager.stopKoboldCpp(); await this.koboldManager.stopKoboldCpp();
await this.sillyTavernManager.cleanup(); return { success: true };
} catch (error) { } catch (error) {
this.logManager.logError('Error during stop/cleanup:', error as Error); return { success: false, error: (error as Error).message };
throw error;
} }
}); });

View file

@ -1,4 +1,3 @@
/* eslint-disable no-comments/disallowComments */
import { spawn, ChildProcess } from 'child_process'; import { spawn, ChildProcess } from 'child_process';
import { join } from 'path'; import { join } from 'path';
import { import {
@ -14,17 +13,17 @@ import {
import { rm } from 'fs/promises'; import { rm } from 'fs/promises';
import { dialog } from 'electron'; import { dialog } from 'electron';
import { terminateProcess } from '@/utils/processUtils';
import { execa } from 'execa'; import { execa } from 'execa';
import { terminateProcess } from '@/utils/process';
import { getROCmDownload } from '@/utils/rocm';
import { got } from 'got'; import { got } from 'got';
import { pipeline } from 'stream/promises'; import { pipeline } from 'stream/promises';
import { ConfigManager } from '@/main/managers/ConfigManager'; import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager'; import { LogManager } from '@/main/managers/LogManager';
import { WindowManager } from '@/main/managers/WindowManager'; import { WindowManager } from '@/main/managers/WindowManager';
import { ROCM, PRODUCT_NAME, GITHUB_API } from '@/constants'; import { PRODUCT_NAME } from '@/constants';
import { stripAssetExtensions } from '@/utils/versionUtils'; import { stripAssetExtensions } from '@/utils/version';
import type { import type {
DownloadItem,
GitHubAsset, GitHubAsset,
InstalledVersion, InstalledVersion,
KoboldConfig, KoboldConfig,
@ -285,7 +284,9 @@ export class KoboldCppManager {
if ( if (
statSync(filePath).isFile() && statSync(filePath).isFile() &&
(file.endsWith('.kcpps') || file.endsWith('.kcppt')) (file.endsWith('.kcpps') ||
file.endsWith('.kcppt') ||
file.endsWith('.json'))
) { ) {
const stats = statSync(filePath); const stats = statSync(filePath);
configFiles.push({ configFiles.push({
@ -323,7 +324,7 @@ export class KoboldCppManager {
} }
async saveConfigFile( async saveConfigFile(
configName: string, configFileName: string,
configData: KoboldConfig configData: KoboldConfig
): Promise<boolean> { ): Promise<boolean> {
try { try {
@ -332,15 +333,7 @@ export class KoboldCppManager {
return false; return false;
} }
let configFileName = `${configName}.kcpps`; const configPath = join(this.installDir, configFileName);
let configPath = join(this.installDir, configFileName);
const kcpptPath = join(this.installDir, `${configName}.kcppt`);
if (existsSync(kcpptPath)) {
configFileName = `${configName}.kcppt`;
configPath = kcpptPath;
}
writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf-8'); writeFileSync(configPath, JSON.stringify(configData, null, 2), 'utf-8');
return true; return true;
} catch (error) { } catch (error) {
@ -555,81 +548,13 @@ export class KoboldCppManager {
return this.koboldProcess !== null && !this.koboldProcess.killed; return this.koboldProcess !== null && !this.koboldProcess.killed;
} }
async getROCmDownload(): Promise<DownloadItem | null> {
const platform = process.platform;
if (platform === 'linux') {
try {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const latestRelease = await response.json();
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version,
type: 'rocm',
};
} catch (error) {
this.logManager.logError(
'Failed to fetch ROCm version info:',
error as Error
);
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version: 'unknown',
type: 'rocm',
};
}
} else if (platform === 'win32') {
return null;
// The launcher doesn't exist in unpacked state yet.
// Enable when it's ready.
// try {
// const response = await fetch(GITHUB_API.ROCM_LATEST_RELEASE_URL);
// if (!response.ok) {
// return null;
// }
// const release = await response.json();
// const rocmAsset = release.assets?.find((asset: GitHubAsset) =>
// asset.name.endsWith('rocm.exe')
// );
// if (rocmAsset) {
// return {
// name: rocmAsset.name,
// url: rocmAsset.browser_download_url,
// size: rocmAsset.size,
// version: release.tag_name?.replace(/^v/, '') || 'unknown',
// type: 'rocm',
// };
// }
// } catch (error) {
// this.logManager.logError(
// 'Failed to fetch Windows ROCm release:',
// error as Error
// );
// }
}
return null;
}
async downloadROCm(onProgress?: (progress: number) => void): Promise<{ async downloadROCm(onProgress?: (progress: number) => void): Promise<{
success: boolean; success: boolean;
path?: string; path?: string;
error?: string; error?: string;
}> { }> {
try { try {
const rocmInfo = await this.getROCmDownload(); const rocmInfo = await getROCmDownload(process.platform);
if (!rocmInfo) { if (!rocmInfo) {
return { return {
success: false, success: false,
@ -740,7 +665,7 @@ export class KoboldCppManager {
): Promise<{ success: boolean; pid?: number; error?: string }> { ): Promise<{ success: boolean; pid?: number; error?: string }> {
try { try {
if (this.koboldProcess) { if (this.koboldProcess) {
this.stopKoboldCpp(); await this.stopKoboldCpp();
} }
const currentVersion = await this.getCurrentVersion(); const currentVersion = await this.getCurrentVersion();
@ -820,34 +745,13 @@ export class KoboldCppManager {
} }
} }
stopKoboldCpp(): void { async stopKoboldCpp(): Promise<void> {
if (this.koboldProcess) { if (this.koboldProcess) {
const pid = this.koboldProcess.pid; await terminateProcess(this.koboldProcess, {
timeoutMs: 5000,
try { logError: (message, error) => this.logManager.logError(message, error),
this.koboldProcess.kill('SIGTERM'); });
this.koboldProcess = null;
setTimeout(() => {
if (this.koboldProcess && !this.koboldProcess.killed) {
try {
this.koboldProcess.kill('SIGKILL');
} catch (error) {
this.logManager.logError(
'Error force-killing KoboldCpp process:',
error as Error
);
}
}
}, 5000);
this.koboldProcess = null;
} catch (error) {
this.logManager.logError(
`Error sending SIGTERM to KoboldCpp process (PID: ${pid}):`,
error as Error
);
this.koboldProcess = null;
}
} }
} }

View file

@ -8,7 +8,7 @@ import type { ChildProcess } from 'child_process';
import { LogManager } from './LogManager'; import { LogManager } from './LogManager';
import { WindowManager } from './WindowManager'; import { WindowManager } from './WindowManager';
import { SILLYTAVERN } from '@/constants'; import { SILLYTAVERN } from '@/constants';
import { terminateProcess, killProcessOnPort } from '@/utils/processUtils'; import { terminateProcess } from '@/utils/process';
export interface SillyTavernConfig { export interface SillyTavernConfig {
name: string; name: string;
@ -109,15 +109,13 @@ export class SillyTavernManager {
return join(this.getSillyTavernDataRoot(), 'default-user', 'settings.json'); return join(this.getSillyTavernDataRoot(), 'default-user', 'settings.json');
} }
private getSillyTavernBaseArgs(): string[] { private static readonly SILLYTAVERN_BASE_ARGS = [
return [ 'sillytavern',
'sillytavern', '--listen',
'--listen', '--browserLaunchEnabled',
'--browserLaunchEnabled', 'false',
'false', '--disableCsrf',
'--disableCsrf', ];
];
}
async isNpxAvailable(): Promise<boolean> { async isNpxAvailable(): Promise<boolean> {
try { try {
@ -167,22 +165,19 @@ export class SillyTavernManager {
'SillyTavern settings not found, starting SillyTavern briefly to generate config...' 'SillyTavern settings not found, starting SillyTavern briefly to generate config...'
); );
const spawnArgs = this.getSillyTavernBaseArgs();
this.windowManager.sendKoboldOutput(
`Running command: npx ${spawnArgs.join(' ')}`
);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const initProcess = this.createNpxProcess(spawnArgs); const initProcess = this.createNpxProcess(
SillyTavernManager.SILLYTAVERN_BASE_ARGS
);
let hasResolved = false; let hasResolved = false;
initProcess.on('exit', (code: number | null, signal: string | null) => { initProcess.on('exit', (code: number | null, signal: string | null) => {
const exitMessage = signal this.windowManager.sendKoboldOutput(
? `SillyTavern init process terminated with signal ${signal}` signal
: `SillyTavern init process exited with code ${code}`; ? `SillyTavern init process terminated with signal ${signal}`
this.windowManager.sendKoboldOutput(exitMessage); : `SillyTavern init process exited with code ${code}`
);
if (!hasResolved) { if (!hasResolved) {
hasResolved = true; hasResolved = true;
@ -227,24 +222,29 @@ export class SillyTavernManager {
const output = data.toString(); const output = data.toString();
if (output.includes('SillyTavern is listening')) { if (output.includes('SillyTavern is listening')) {
setTimeout(() => { setTimeout(async () => {
if (!initProcess.killed && !hasResolved) { if (!initProcess.killed && !hasResolved) {
hasResolved = true; hasResolved = true;
initProcess.kill('SIGTERM');
await terminateProcess(initProcess, {
logError: (message, error) =>
this.logManager.logError(message, error),
});
this.windowManager.sendKoboldOutput( this.windowManager.sendKoboldOutput(
'SillyTavern settings should now be generated' 'SillyTavern settings should now be generated'
); );
resolve(); resolve();
} }
}, 1000); }, 2000);
} }
}); });
} }
if (initProcess.stderr) { if (initProcess.stderr) {
initProcess.stderr.on('data', (data: Buffer) => { initProcess.stderr.on('data', (data: Buffer) => {
const output = data.toString(); this.windowManager.sendKoboldOutput(data.toString().trim());
this.windowManager.sendKoboldOutput(output.trim());
}); });
} }
}); });
@ -296,7 +296,6 @@ export class SillyTavernManager {
} }
const koboldUrl = `http://${koboldHost}:${koboldPort}`; const koboldUrl = `http://${koboldHost}:${koboldPort}`;
const koboldApiUrl = `${koboldUrl}/api`;
if (!settings.power_user) settings.power_user = {}; if (!settings.power_user) settings.power_user = {};
const powerUser = settings.power_user as Record<string, unknown>; const powerUser = settings.power_user as Record<string, unknown>;
@ -310,7 +309,7 @@ export class SillyTavernManager {
if (!textgenSettings.server_urls) textgenSettings.server_urls = {}; if (!textgenSettings.server_urls) textgenSettings.server_urls = {};
const serverUrls = textgenSettings.server_urls as Record<string, unknown>; const serverUrls = textgenSettings.server_urls as Record<string, unknown>;
serverUrls.koboldcpp = koboldApiUrl; serverUrls.koboldcpp = koboldUrl;
settings.main_api = 'textgenerationwebui'; settings.main_api = 'textgenerationwebui';
textgenSettings.type = 'koboldcpp'; textgenSettings.type = 'koboldcpp';
@ -341,8 +340,7 @@ export class SillyTavernManager {
}, 30000); }, 30000);
const checkForOutput = (data: Buffer) => { const checkForOutput = (data: Buffer) => {
const output = data.toString(); if (data.toString().includes('SillyTavern is listening')) {
if (output.includes('SillyTavern is listening')) {
clearTimeout(timeout); clearTimeout(timeout);
this.windowManager.sendKoboldOutput('SillyTavern is now running!'); this.windowManager.sendKoboldOutput('SillyTavern is now running!');
resolve(); resolve();
@ -414,8 +412,6 @@ export class SillyTavernManager {
await this.stopFrontend(); await this.stopFrontend();
await killProcessOnPort(config.port);
this.windowManager.sendKoboldOutput( this.windowManager.sendKoboldOutput(
`Preparing SillyTavern to connect to KoboldCpp at ${koboldHost}:${koboldPort}...` `Preparing SillyTavern to connect to KoboldCpp at ${koboldHost}:${koboldPort}...`
); );
@ -428,7 +424,7 @@ export class SillyTavernManager {
); );
const sillyTavernArgs = [ const sillyTavernArgs = [
...this.getSillyTavernBaseArgs(), ...SillyTavernManager.SILLYTAVERN_BASE_ARGS,
'--port', '--port',
config.port.toString(), config.port.toString(),
]; ];
@ -436,14 +432,12 @@ export class SillyTavernManager {
this.windowManager.sendKoboldOutput( this.windowManager.sendKoboldOutput(
'Final port check before starting SillyTavern...' 'Final port check before starting SillyTavern...'
); );
await killProcessOnPort(config.port);
this.sillyTavernProcess = this.createNpxProcess(sillyTavernArgs); this.sillyTavernProcess = this.createNpxProcess(sillyTavernArgs);
if (this.sillyTavernProcess.stdout) { if (this.sillyTavernProcess.stdout) {
this.sillyTavernProcess.stdout.on('data', (data: Buffer) => { this.sillyTavernProcess.stdout.on('data', (data: Buffer) => {
const output = data.toString(); this.windowManager.sendKoboldOutput(data.toString(), true);
this.windowManager.sendKoboldOutput(output, true);
}); });
} }
@ -491,10 +485,6 @@ export class SillyTavernManager {
this.windowManager.sendKoboldOutput( this.windowManager.sendKoboldOutput(
`Killing processes on port ${SILLYTAVERN.PORT}...` `Killing processes on port ${SILLYTAVERN.PORT}...`
); );
await killProcessOnPort(SILLYTAVERN.PORT);
this.windowManager.sendKoboldOutput(
`Port ${SILLYTAVERN.PORT} is now free`
);
} catch (error) { } catch (error) {
this.logManager.logError( this.logManager.logError(
'Error killing processes on port:', 'Error killing processes on port:',
@ -531,6 +521,7 @@ export class SillyTavernManager {
await Promise.all(promises); await Promise.all(promises);
this.windowManager.sendKoboldOutput('SillyTavern frontend stopped'); this.windowManager.sendKoboldOutput('SillyTavern frontend stopped');
} }
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
if (this.sillyTavernProcess) { if (this.sillyTavernProcess) {
try { try {

View file

@ -2,6 +2,7 @@
import si from 'systeminformation'; import si from 'systeminformation';
import { shortenDeviceName } from '@/utils/hardware'; import { shortenDeviceName } from '@/utils/hardware';
import { LogManager } from '@/main/managers/LogManager'; import { LogManager } from '@/main/managers/LogManager';
import { terminateProcess } from '@/utils/process';
import type { import type {
CPUCapabilities, CPUCapabilities,
GPUCapabilities, GPUCapabilities,
@ -184,12 +185,8 @@ export class HardwareService {
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}); });
setTimeout(() => { setTimeout(async () => {
try { await terminateProcess(nvidia);
nvidia.kill('SIGTERM');
} catch {
void 0;
}
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}, 5000); }, 5000);
}); });
@ -266,12 +263,8 @@ export class HardwareService {
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}); });
setTimeout(() => { setTimeout(async () => {
try { await terminateProcess(rocminfo);
rocminfo.kill('SIGTERM');
} catch {
void 0;
}
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}, 5000); }, 5000);
}); });
@ -325,12 +318,8 @@ export class HardwareService {
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}); });
setTimeout(() => { setTimeout(async () => {
try { await terminateProcess(vulkaninfo);
vulkaninfo.kill('SIGTERM');
} catch {
void 0;
}
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}, 5000); }, 5000);
}); });
@ -426,12 +415,8 @@ export class HardwareService {
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}); });
setTimeout(() => { setTimeout(async () => {
try { await terminateProcess(clinfo);
clinfo.kill('SIGTERM');
} catch {
void 0;
}
resolve({ supported: false, devices: [] }); resolve({ supported: false, devices: [] });
}, 3000); }, 3000);
}); });

View file

@ -1,5 +1,5 @@
import { ASSET_SUFFIXES } from '@/constants'; import { ASSET_SUFFIXES } from '@/constants';
import { stripAssetExtensions } from '@/utils/versionUtils'; import { stripAssetExtensions } from '@/utils/version';
export const getAssetDescription = (assetName: string): string => { export const getAssetDescription = (assetName: string): string => {
const name = stripAssetExtensions(assetName).toLowerCase(); const name = stripAssetExtensions(assetName).toLowerCase();

17
src/utils/download.ts Normal file
View file

@ -0,0 +1,17 @@
import { ROCM } from '@/constants';
export const formatDownloadSize = (size: number, url?: string): string => {
if (!size) return '';
const isApproximateSize = url?.includes(ROCM.DOWNLOAD_URL);
return isApproximateSize
? `~${formatFileSizeInMB(size)}`
: formatFileSizeInMB(size);
};
const formatFileSizeInMB = (bytes: number) => {
if (bytes === 0) return '0 MB';
const mb = bytes / (1024 * 1024);
return parseFloat(mb.toFixed(1)) + ' MB';
};

View file

@ -1,26 +0,0 @@
const URL_REGEX = /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/gi;
export const linkifyText = (text: string): string =>
text.replace(URL_REGEX, (url) => {
const cleanUrl = url.replace(/[.,;:!?]+$/, '');
const trailingPunctuation = url.slice(cleanUrl.length);
return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #339af0; text-decoration: underline; cursor: pointer;">${cleanUrl}</a>${trailingPunctuation}`;
});
export const escapeHtmlExceptLinks = (text: string): string => {
const escaped = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
return linkifyText(escaped);
};
export const processTerminalContent = (content: string): string => {
if (!content) return '';
return escapeHtmlExceptLinks(content);
};

42
src/utils/process.ts Normal file
View file

@ -0,0 +1,42 @@
import type { ChildProcess } from 'child_process';
export interface ProcessTerminationOptions {
timeoutMs?: number;
logError?: (message: string, error: Error) => void;
}
export async function terminateProcess(
childProcess: ChildProcess,
options: ProcessTerminationOptions = {}
): Promise<void> {
const { timeoutMs = 3000, logError } = options;
if (!childProcess?.pid) {
return;
}
try {
const signal = process.platform === 'win32' ? undefined : 'SIGTERM';
childProcess.kill(signal);
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
if (childProcess && !childProcess.killed) {
try {
childProcess.kill('SIGKILL');
} catch (error) {
logError?.('Error force-killing process:', error as Error);
}
}
resolve();
}, timeoutMs);
childProcess.once('exit', () => {
clearTimeout(timeout);
resolve();
});
});
} catch (error) {
logError?.('Error terminating process:', error as Error);
}
}

View file

@ -1,100 +0,0 @@
import type { ChildProcess } from 'child_process';
import { exec } from 'child_process';
import fkill from 'fkill';
export interface ProcessTerminationOptions {
logError?: (message: string, error: Error) => void;
}
async function findProcessesOnPort(port: number): Promise<number[]> {
return new Promise((resolve) => {
if (process.platform === 'win32') {
exec(`netstat -ano | findstr :${port}`, (error, stdout) => {
if (error || !stdout) {
resolve([]);
return;
}
const pids: number[] = [];
const lines = stdout.split('\n');
for (const line of lines) {
const match = line.match(/\s+(\d+)$/);
if (match) {
const pid = parseInt(match[1], 10);
if (!isNaN(pid) && pid > 0) {
pids.push(pid);
}
}
}
resolve([...new Set(pids)]);
});
} else {
exec(`lsof -ti:${port}`, (error, stdout) => {
if (error || !stdout) {
resolve([]);
return;
}
const pids = stdout
.trim()
.split('\n')
.map((pid) => parseInt(pid, 10))
.filter((pid) => !isNaN(pid) && pid > 0);
resolve(pids);
});
}
});
}
async function isPortFree(port: number): Promise<boolean> {
const pids = await findProcessesOnPort(port);
return pids.length === 0;
}
export async function killProcessOnPort(port: number): Promise<void> {
try {
await fkill(`:${port}`, { force: true, silent: true });
} catch {
void 0;
}
const pids = await findProcessesOnPort(port);
if (pids.length > 0) {
for (const pid of pids) {
try {
await fkill(pid, { force: true, silent: true });
} catch {
void 0;
}
}
}
let attempts = 0;
const maxAttempts = 10;
while (attempts < maxAttempts && !(await isPortFree(port))) {
await new Promise((resolve) => setTimeout(resolve, 1000));
attempts++;
}
}
export async function terminateProcess(
process: ChildProcess,
options: ProcessTerminationOptions = {}
): Promise<void> {
const { logError } = options;
if (!process || !process.pid) {
return;
}
try {
await fkill(process.pid, { force: true, silent: true });
} catch (error) {
logError?.('Error terminating process:', error as Error);
}
}

36
src/utils/rocm.ts Normal file
View file

@ -0,0 +1,36 @@
import { GITHUB_API, ROCM } from '@/constants';
import type { DownloadItem } from '@/types/electron';
export async function getROCmDownload(
platform: string
): Promise<DownloadItem | null> {
if (platform !== 'linux') {
return null;
}
try {
const response = await fetch(GITHUB_API.LATEST_RELEASE_URL);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const latestRelease = await response.json();
const version = latestRelease?.tag_name?.replace(/^v/, '') || 'unknown';
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version,
type: 'rocm',
};
} catch {
return {
name: ROCM.BINARY_NAME,
url: ROCM.DOWNLOAD_URL,
size: ROCM.SIZE_BYTES_APPROX,
version: 'unknown',
type: 'rocm',
};
}
}

View file

@ -24,3 +24,30 @@ export const handleTerminalOutput = (
return prevContent + newData; return prevContent + newData;
} }
}; };
const URL_REGEX = /(https?:\/\/[^\s<>"{}|\\^`[\]]+)/gi;
const linkifyText = (text: string): string =>
text.replace(URL_REGEX, (url) => {
const cleanUrl = url.replace(/[.,;:!?]+$/, '');
const trailingPunctuation = url.slice(cleanUrl.length);
return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer" style="color: #339af0; text-decoration: underline; cursor: pointer;">${cleanUrl}</a>${trailingPunctuation}`;
});
const escapeHtmlExceptLinks = (text: string): string => {
const escaped = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
return linkifyText(escaped);
};
export const processTerminalContent = (content: string): string => {
if (!content) return '';
return escapeHtmlExceptLinks(content);
};

View file

@ -1,15 +1,25 @@
import { ROCM } from '@/constants'; import type { InstalledVersion } from '@/types';
export const formatDownloadSize = (size: number, url?: string): string => { export const getDisplayNameFromPath = (
if (!size) return ''; installedVersion: InstalledVersion
): string => {
const pathParts = installedVersion.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex(
(part) => part === 'koboldcpp-launcher' || part === 'koboldcpp-launcher.exe'
);
const isApproximateSize = url?.includes(ROCM.DOWNLOAD_URL); if (launcherIndex > 0) {
return pathParts[launcherIndex - 1];
}
return isApproximateSize return installedVersion.filename;
? `~${formatFileSizeInMB(size)}`
: formatFileSizeInMB(size);
}; };
export const stripAssetExtensions = (assetName: string): string =>
assetName
.replace(/\.(tar\.gz|zip|exe|dmg|AppImage)$/i, '')
.replace(/\.packed$/, '');
export const compareVersions = (versionA: string, versionB: string): number => { export const compareVersions = (versionA: string, versionB: string): number => {
const cleanVersion = (version: string): string => const cleanVersion = (version: string): string =>
version.replace(/^v/, '').replace(/[^0-9.]/g, ''); version.replace(/^v/, '').replace(/[^0-9.]/g, '');
@ -34,9 +44,3 @@ export const compareVersions = (versionA: string, versionB: string): number => {
return 0; return 0;
}; };
const formatFileSizeInMB = (bytes: number) => {
if (bytes === 0) return '0 MB';
const mb = bytes / (1024 * 1024);
return parseFloat(mb.toFixed(1)) + ' MB';
};

View file

@ -1,21 +0,0 @@
import type { InstalledVersion } from '@/types';
export const getDisplayNameFromPath = (
installedVersion: InstalledVersion
): string => {
const pathParts = installedVersion.path.split(/[/\\]/);
const launcherIndex = pathParts.findIndex(
(part) => part === 'koboldcpp-launcher' || part === 'koboldcpp-launcher.exe'
);
if (launcherIndex > 0) {
return pathParts[launcherIndex - 1];
}
return installedVersion.filename;
};
export const stripAssetExtensions = (assetName: string): string =>
assetName
.replace(/\.(tar\.gz|zip|exe|dmg|AppImage)$/i, '')
.replace(/\.packed$/, '');

194
yarn.lock
View file

@ -1601,16 +1601,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"aggregate-error@npm:^5.0.0":
version: 5.0.0
resolution: "aggregate-error@npm:5.0.0"
dependencies:
clean-stack: "npm:^5.2.0"
indent-string: "npm:^5.0.0"
checksum: 10c0/a5de7138571f514bad76290736f49a0db8809247082f2519037e0c37d03fc8d91d733e079d6b1674feda28a757b1932421ad205b8c0f8794a0c0e5bf1be2315e
languageName: node
linkType: hard
"ajv-keywords@npm:^3.4.1": "ajv-keywords@npm:^3.4.1":
version: 3.5.2 version: 3.5.2
resolution: "ajv-keywords@npm:3.5.2" resolution: "ajv-keywords@npm:3.5.2"
@ -2237,15 +2227,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"clean-stack@npm:^5.2.0":
version: 5.2.0
resolution: "clean-stack@npm:5.2.0"
dependencies:
escape-string-regexp: "npm:5.0.0"
checksum: 10c0/0de47a4152e49dcdeede5f47d7bb9a39a3ea748acb1cd2f0160dbee972d920be81390cb4c5566e6b795791b9efb12359e89fdd7c2e63b36025d59529558570f1
languageName: node
linkType: hard
"cli-cursor@npm:^3.1.0": "cli-cursor@npm:^3.1.0":
version: 3.1.0 version: 3.1.0
resolution: "cli-cursor@npm:3.1.0" resolution: "cli-cursor@npm:3.1.0"
@ -2467,7 +2448,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": "cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.6":
version: 7.0.6 version: 7.0.6
resolution: "cross-spawn@npm:7.0.6" resolution: "cross-spawn@npm:7.0.6"
dependencies: dependencies:
@ -3117,13 +3098,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"escape-string-regexp@npm:5.0.0":
version: 5.0.0
resolution: "escape-string-regexp@npm:5.0.0"
checksum: 10c0/6366f474c6f37a802800a435232395e04e9885919873e382b157ab7e8f0feb8fed71497f84a6f6a81a49aab41815522f5839112bd38026d203aea0c91622df95
languageName: node
linkType: hard
"escape-string-regexp@npm:^4.0.0": "escape-string-regexp@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "escape-string-regexp@npm:4.0.0" resolution: "escape-string-regexp@npm:4.0.0"
@ -3380,40 +3354,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"execa@npm:^6.1.0":
version: 6.1.0
resolution: "execa@npm:6.1.0"
dependencies:
cross-spawn: "npm:^7.0.3"
get-stream: "npm:^6.0.1"
human-signals: "npm:^3.0.1"
is-stream: "npm:^3.0.0"
merge-stream: "npm:^2.0.0"
npm-run-path: "npm:^5.1.0"
onetime: "npm:^6.0.0"
signal-exit: "npm:^3.0.7"
strip-final-newline: "npm:^3.0.0"
checksum: 10c0/004ee32092af745766a1b0352fdba8701a4001bc3fe08e63101c04276d4c860bbe11bb8ab85f37acdff13d3da83d60e044041dcf24bd7e25e645a543828d9c41
languageName: node
linkType: hard
"execa@npm:^8.0.1":
version: 8.0.1
resolution: "execa@npm:8.0.1"
dependencies:
cross-spawn: "npm:^7.0.3"
get-stream: "npm:^8.0.1"
human-signals: "npm:^5.0.0"
is-stream: "npm:^3.0.0"
merge-stream: "npm:^2.0.0"
npm-run-path: "npm:^5.1.0"
onetime: "npm:^6.0.0"
signal-exit: "npm:^4.1.0"
strip-final-newline: "npm:^3.0.0"
checksum: 10c0/2c52d8775f5bf103ce8eec9c7ab3059909ba350a5164744e9947ed14a53f51687c040a250bda833f906d1283aa8803975b84e6c8f7a7c42f99dc8ef80250d1af
languageName: node
linkType: hard
"execa@npm:^9.6.0": "execa@npm:^9.6.0":
version: 9.6.0 version: 9.6.0
resolution: "execa@npm:9.6.0" resolution: "execa@npm:9.6.0"
@ -3591,20 +3531,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fkill@npm:^9.0.0":
version: 9.0.0
resolution: "fkill@npm:9.0.0"
dependencies:
aggregate-error: "npm:^5.0.0"
execa: "npm:^8.0.1"
pid-port: "npm:^1.0.0"
process-exists: "npm:^5.0.0"
ps-list: "npm:^8.1.1"
taskkill: "npm:^5.0.0"
checksum: 10c0/91090ef26287b833d810789ef828b0fb37458677ea9c7b41a294e20a1147692d80e8ba61fa5a943211d1a0fa9331503f634f30a0f931a794c5be6a635296e90b
languageName: node
linkType: hard
"flat-cache@npm:^4.0.0": "flat-cache@npm:^4.0.0":
version: 4.0.1 version: 4.0.1
resolution: "flat-cache@npm:4.0.1" resolution: "flat-cache@npm:4.0.1"
@ -3693,7 +3619,6 @@ __metadata:
eslint-plugin-react-refresh: "npm:^0.4.20" eslint-plugin-react-refresh: "npm:^0.4.20"
eslint-plugin-sonarjs: "npm:^3.0.5" eslint-plugin-sonarjs: "npm:^3.0.5"
execa: "npm:^9.6.0" execa: "npm:^9.6.0"
fkill: "npm:^9.0.0"
globals: "npm:^16.3.0" globals: "npm:^16.3.0"
got: "npm:^14.4.7" got: "npm:^14.4.7"
husky: "npm:^9.1.7" husky: "npm:^9.1.7"
@ -3902,20 +3827,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"get-stream@npm:^6.0.1":
version: 6.0.1
resolution: "get-stream@npm:6.0.1"
checksum: 10c0/49825d57d3fd6964228e6200a58169464b8e8970489b3acdc24906c782fb7f01f9f56f8e6653c4a50713771d6658f7cfe051e5eb8c12e334138c9c918b296341
languageName: node
linkType: hard
"get-stream@npm:^8.0.1":
version: 8.0.1
resolution: "get-stream@npm:8.0.1"
checksum: 10c0/5c2181e98202b9dae0bb4a849979291043e5892eb40312b47f0c22b9414fc9b28a3b6063d2375705eb24abc41ecf97894d9a51f64ff021511b504477b27b4290
languageName: node
linkType: hard
"get-stream@npm:^9.0.0, get-stream@npm:^9.0.1": "get-stream@npm:^9.0.0, get-stream@npm:^9.0.1":
version: 9.0.1 version: 9.0.1
resolution: "get-stream@npm:9.0.1" resolution: "get-stream@npm:9.0.1"
@ -4229,20 +4140,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"human-signals@npm:^3.0.1":
version: 3.0.1
resolution: "human-signals@npm:3.0.1"
checksum: 10c0/0bb27e72aea1666322f69ab9816e05df952ef2160346f2293f98f45d472edb1b62d0f1a596697b50d48d8f8222e6db3b9f9dc0b6bf6113866121001f0a8e48e9
languageName: node
linkType: hard
"human-signals@npm:^5.0.0":
version: 5.0.0
resolution: "human-signals@npm:5.0.0"
checksum: 10c0/5a9359073fe17a8b58e5a085e9a39a950366d9f00217c4ff5878bd312e09d80f460536ea6a3f260b5943a01fe55c158d1cea3fc7bee3d0520aeef04f6d915c82
languageName: node
linkType: hard
"human-signals@npm:^8.0.1": "human-signals@npm:^8.0.1":
version: 8.0.1 version: 8.0.1
resolution: "human-signals@npm:8.0.1" resolution: "human-signals@npm:8.0.1"
@ -4332,13 +4229,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"indent-string@npm:^5.0.0":
version: 5.0.0
resolution: "indent-string@npm:5.0.0"
checksum: 10c0/8ee77b57d92e71745e133f6f444d6fa3ed503ad0e1bcd7e80c8da08b42375c07117128d670589725ed07b1978065803fa86318c309ba45415b7fe13e7f170220
languageName: node
linkType: hard
"infer-owner@npm:^1.0.4": "infer-owner@npm:^1.0.4":
version: 1.0.4 version: 1.0.4
resolution: "infer-owner@npm:1.0.4" resolution: "infer-owner@npm:1.0.4"
@ -4624,13 +4514,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-stream@npm:^3.0.0":
version: 3.0.0
resolution: "is-stream@npm:3.0.0"
checksum: 10c0/eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8
languageName: node
linkType: hard
"is-stream@npm:^4.0.1": "is-stream@npm:^4.0.1":
version: 4.0.1 version: 4.0.1
resolution: "is-stream@npm:4.0.1" resolution: "is-stream@npm:4.0.1"
@ -5188,13 +5071,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"merge-stream@npm:^2.0.0":
version: 2.0.0
resolution: "merge-stream@npm:2.0.0"
checksum: 10c0/867fdbb30a6d58b011449b8885601ec1690c3e41c759ecd5a9d609094f7aed0096c37823ff4a7190ef0b8f22cc86beb7049196ff68c016e3b3c671d0dac91ce5
languageName: node
linkType: hard
"merge2@npm:^1.3.0": "merge2@npm:^1.3.0":
version: 1.4.1 version: 1.4.1
resolution: "merge2@npm:1.4.1" resolution: "merge2@npm:1.4.1"
@ -5244,13 +5120,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mimic-fn@npm:^4.0.0":
version: 4.0.0
resolution: "mimic-fn@npm:4.0.0"
checksum: 10c0/de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf
languageName: node
linkType: hard
"mimic-function@npm:^5.0.0": "mimic-function@npm:^5.0.0":
version: 5.0.1 version: 5.0.1
resolution: "mimic-function@npm:5.0.1" resolution: "mimic-function@npm:5.0.1"
@ -5598,15 +5467,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"npm-run-path@npm:^5.1.0":
version: 5.3.0
resolution: "npm-run-path@npm:5.3.0"
dependencies:
path-key: "npm:^4.0.0"
checksum: 10c0/124df74820c40c2eb9a8612a254ea1d557ddfab1581c3e751f825e3e366d9f00b0d76a3c94ecd8398e7f3eee193018622677e95816e8491f0797b21e30b2deba
languageName: node
linkType: hard
"npm-run-path@npm:^6.0.0": "npm-run-path@npm:^6.0.0":
version: 6.0.0 version: 6.0.0
resolution: "npm-run-path@npm:6.0.0" resolution: "npm-run-path@npm:6.0.0"
@ -5733,15 +5593,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"onetime@npm:^6.0.0":
version: 6.0.0
resolution: "onetime@npm:6.0.0"
dependencies:
mimic-fn: "npm:^4.0.0"
checksum: 10c0/4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c
languageName: node
linkType: hard
"onetime@npm:^7.0.0": "onetime@npm:^7.0.0":
version: 7.0.0 version: 7.0.0
resolution: "onetime@npm:7.0.0" resolution: "onetime@npm:7.0.0"
@ -5955,15 +5806,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pid-port@npm:^1.0.0":
version: 1.0.2
resolution: "pid-port@npm:1.0.2"
dependencies:
execa: "npm:^8.0.1"
checksum: 10c0/b3a62fde1c73f896a0950f5446b014b55fff2bd84068bce3cba4a459561f695de76c11f74a6c54a64f395b0eca29c79f5c3d705061067d3d157ae8115b00e8a3
languageName: node
linkType: hard
"pidtree@npm:^0.6.0": "pidtree@npm:^0.6.0":
version: 0.6.0 version: 0.6.0
resolution: "pidtree@npm:0.6.0" resolution: "pidtree@npm:0.6.0"
@ -6041,15 +5883,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"process-exists@npm:^5.0.0":
version: 5.0.0
resolution: "process-exists@npm:5.0.0"
dependencies:
ps-list: "npm:^8.0.0"
checksum: 10c0/2d0edce88dbf22adc1d30aac08a996bd747786cb91c861b003127345f27217e89c528c2c661404e2ad3d50c2a598e5ae4bbb997ce3628e095a07596590a29b67
languageName: node
linkType: hard
"progress@npm:^2.0.3": "progress@npm:^2.0.3":
version: 2.0.3 version: 2.0.3
resolution: "progress@npm:2.0.3" resolution: "progress@npm:2.0.3"
@ -6085,13 +5918,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ps-list@npm:^8.0.0, ps-list@npm:^8.1.1":
version: 8.1.1
resolution: "ps-list@npm:8.1.1"
checksum: 10c0/5c0fa265f910e03eac14e0785f106d481c4bc3eb95f6fd77a055c231194a5accdea5e1e410050bdb5976cf1620a0bcf41923a10c9540e3ad1b9c8e0bd402116b
languageName: node
linkType: hard
"pump@npm:^3.0.0": "pump@npm:^3.0.0":
version: 3.0.3 version: 3.0.3
resolution: "pump@npm:3.0.3" resolution: "pump@npm:3.0.3"
@ -6805,7 +6631,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.7": "signal-exit@npm:^3.0.2":
version: 3.0.7 version: 3.0.7
resolution: "signal-exit@npm:3.0.7" resolution: "signal-exit@npm:3.0.7"
checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912
@ -7130,13 +6956,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"strip-final-newline@npm:^3.0.0":
version: 3.0.0
resolution: "strip-final-newline@npm:3.0.0"
checksum: 10c0/a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce
languageName: node
linkType: hard
"strip-final-newline@npm:^4.0.0": "strip-final-newline@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "strip-final-newline@npm:4.0.0" resolution: "strip-final-newline@npm:4.0.0"
@ -7221,15 +7040,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"taskkill@npm:^5.0.0":
version: 5.0.0
resolution: "taskkill@npm:5.0.0"
dependencies:
execa: "npm:^6.1.0"
checksum: 10c0/ba9684224806a3b0bbc1898a51125d56aa6fa16f4e9e5dfbb14bc00301151297414966067a28cb5078cb5e7db8161b9b1c95a4969d0846078f7f7582fe633e46
languageName: node
linkType: hard
"temp-file@npm:^3.4.0": "temp-file@npm:^3.4.0":
version: 3.4.0 version: 3.4.0
resolution: "temp-file@npm:3.4.0" resolution: "temp-file@npm:3.4.0"