WIP: special sillytavern config changes for windows

This commit is contained in:
lone-cloud 2025-08-29 00:07:26 -07:00
parent 2ecbef9108
commit 07ebcb182e
17 changed files with 607 additions and 274 deletions

View file

@ -83,6 +83,7 @@
"@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

@ -98,27 +98,17 @@ export const App = () => {
}; };
checkInstallation(); checkInstallation();
// eslint-disable-next-line react-hooks/exhaustive-deps
const cleanupInstallDirListener = }, []);
window.electronAPI.kobold.onInstallDirChanged(() => {
checkInstallation();
});
const cleanupVersionsListener = window.electronAPI.kobold.onVersionsUpdated(
() => {
checkInstallation();
}
);
return () => {
cleanupInstallDirListener();
cleanupVersionsListener();
};
}, [checkForUpdates]);
const handleBinaryUpdate = async (download: DownloadItem) => { const handleBinaryUpdate = async (download: DownloadItem) => {
try { try {
const success = await sharedHandleDownload('asset', download, true, true); const success = await sharedHandleDownload({
type: 'asset',
item: download,
isUpdate: true,
wasCurrentBinary: true,
});
if (success) { if (success) {
dismissUpdate(); dismissUpdate();
@ -286,6 +276,7 @@ export const App = () => {
activeTab={activeInterfaceTab} activeTab={activeInterfaceTab}
onTabChange={setActiveInterfaceTab} onTabChange={setActiveInterfaceTab}
isImageGenerationMode={isImageGenerationMode} isImageGenerationMode={isImageGenerationMode}
frontendPreference={frontendPreference}
/> />
</ScreenTransition> </ScreenTransition>
</> </>

View file

@ -55,7 +55,7 @@ export const DownloadCard = ({
const buttons = []; const buttons = [];
if (!isInstalled) { if (!isInstalled) {
buttons.push( return (
<Button <Button
key="download" key="download"
variant="filled" variant="filled"
@ -76,7 +76,7 @@ export const DownloadCard = ({
); );
} }
if (isInstalled && !isCurrent && onMakeCurrent) { if (!isCurrent && onMakeCurrent) {
buttons.push( buttons.push(
<Button <Button
key="makeCurrent" key="makeCurrent"

View file

@ -34,12 +34,12 @@ export const DownloadScreen = ({ onDownloadComplete }: DownloadScreenProps) => {
setDownloadingAsset(download.name); setDownloadingAsset(download.name);
try { try {
const success = await sharedHandleDownload( const success = await sharedHandleDownload({
'asset', type: 'asset',
download, item: download,
false, isUpdate: false,
false wasCurrentBinary: false,
); });
if (success) { if (success) {
onDownloadComplete(); onDownloadComplete();

View file

@ -1,4 +1,4 @@
import { useState, useCallback, useEffect } from 'react'; import { useState, useCallback } from 'react';
import { ServerTab } from '@/components/screens/Interface/ServerTab'; import { ServerTab } from '@/components/screens/Interface/ServerTab';
import { TerminalTab } from '@/components/screens/Interface/TerminalTab'; import { TerminalTab } from '@/components/screens/Interface/TerminalTab';
import type { InterfaceTab, FrontendPreference } from '@/types'; import type { InterfaceTab, FrontendPreference } from '@/types';
@ -7,35 +7,17 @@ interface InterfaceScreenProps {
activeTab?: InterfaceTab | null; activeTab?: InterfaceTab | null;
onTabChange?: (tab: InterfaceTab) => void; onTabChange?: (tab: InterfaceTab) => void;
isImageGenerationMode?: boolean; isImageGenerationMode?: boolean;
frontendPreference?: FrontendPreference;
} }
export const InterfaceScreen = ({ export const InterfaceScreen = ({
activeTab, activeTab,
onTabChange, onTabChange,
isImageGenerationMode = false, isImageGenerationMode = false,
frontendPreference = 'koboldcpp',
}: InterfaceScreenProps) => { }: InterfaceScreenProps) => {
const [serverUrl, setServerUrl] = useState<string>(''); const [serverUrl, setServerUrl] = useState<string>('');
const [isServerReady, setIsServerReady] = useState<boolean>(false); const [isServerReady, setIsServerReady] = useState<boolean>(false);
const [frontendPreference, setFrontendPreference] =
useState<FrontendPreference>('koboldcpp');
useEffect(() => {
const loadFrontendPreference = async () => {
try {
const frontendPreference = (await window.electronAPI.config.get(
'frontendPreference'
)) as FrontendPreference;
setFrontendPreference(frontendPreference || 'koboldcpp');
} catch (error) {
window.electronAPI.logs.logError(
'Failed to load frontend preference:',
error as Error
);
}
};
loadFrontendPreference();
}, []);
const handleServerReady = useCallback( const handleServerReady = useCallback(
(url: string) => { (url: string) => {

View file

@ -30,6 +30,8 @@ export const AdvancedTab = () => {
} | null>(null); } | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const isGpuBackend = backend === 'cuda' || backend === 'rocm';
useEffect(() => { useEffect(() => {
const detectBackendSupport = async () => { const detectBackendSupport = async () => {
try { try {
@ -99,27 +101,27 @@ export const AdvancedTab = () => {
<Group gap="lg" align="flex-start" wrap="nowrap"> <Group gap="lg" align="flex-start" wrap="nowrap">
<CheckboxWithTooltip <CheckboxWithTooltip
checked={quantmatmul} checked={quantmatmul && isGpuBackend}
onChange={handleQuantmatmulChange} onChange={handleQuantmatmulChange}
label="QuantMatMul" label="QuantMatMul"
tooltip={ tooltip={
backend !== 'cuda' && backend !== 'rocm' !isGpuBackend
? 'QuantMatMul is only available for CUDA and ROCm backends.' ? 'QuantMatMul is only available for CUDA and ROCm backends.'
: 'Enable MMQ mode to use finetuned kernels instead of default CuBLAS/HipBLAS for prompt processing.' : 'Enable MMQ mode to use finetuned kernels instead of default CuBLAS/HipBLAS for prompt processing.'
} }
disabled={backend !== 'cuda' && backend !== 'rocm'} disabled={!isGpuBackend}
/> />
<CheckboxWithTooltip <CheckboxWithTooltip
checked={lowvram} checked={lowvram && isGpuBackend}
onChange={handleLowvramChange} onChange={handleLowvramChange}
label="Low VRAM" label="Low VRAM"
tooltip={ tooltip={
backend !== 'cuda' && backend !== 'rocm' !isGpuBackend
? 'Low VRAM mode is only available for CUDA and ROCm backends.' ? 'Low VRAM mode is only available for CUDA and ROCm backends.'
: 'Avoid offloading KV Cache or scratch buffers to VRAM. Allows more layers to fit, but may result in a speed loss.' : 'Avoid offloading KV Cache or scratch buffers to VRAM. Allows more layers to fit, but may result in a speed loss.'
} }
disabled={backend !== 'cuda' && backend !== 'rocm'} disabled={!isGpuBackend}
/> />
</Group> </Group>
</Stack> </Stack>

View file

@ -209,12 +209,12 @@ export const VersionsTab = () => {
throw new Error('Download not found'); throw new Error('Download not found');
} }
const success = await sharedHandleDownload( const success = await sharedHandleDownload({
'asset', type: 'asset',
download, item: download,
false, isUpdate: false,
false wasCurrentBinary: false,
); });
if (success) { if (success) {
await loadInstalledVersions(); await loadInstalledVersions();
@ -231,13 +231,12 @@ export const VersionsTab = () => {
throw new Error('Download not found'); throw new Error('Download not found');
} }
const wasCurrentBinary = version.isCurrent; const success = await sharedHandleDownload({
const success = await sharedHandleDownload( type: 'asset',
'asset', item: download,
download, isUpdate: true,
true, wasCurrentBinary: version.isCurrent,
wasCurrentBinary });
);
if (success) { if (success) {
await loadInstalledVersions(); await loadInstalledVersions();

View file

@ -20,6 +20,13 @@ interface CachedReleaseData {
timestamp: number; timestamp: number;
} }
interface HandleDownloadParams {
type: 'asset' | 'rocm';
item?: DownloadItem;
isUpdate?: boolean;
wasCurrentBinary?: boolean;
}
const CACHE_KEY = 'kobold-releases-cache'; const CACHE_KEY = 'kobold-releases-cache';
const CACHE_DURATION = 60000; const CACHE_DURATION = 60000;
@ -182,12 +189,7 @@ interface UseKoboldVersionsReturn {
downloadProgress: Record<string, number>; downloadProgress: Record<string, number>;
loadRemoteVersions: () => Promise<void>; loadRemoteVersions: () => Promise<void>;
refresh: () => Promise<void>; refresh: () => Promise<void>;
handleDownload: ( handleDownload: (params: HandleDownloadParams) => Promise<boolean>;
type: 'asset' | 'rocm',
item?: DownloadItem,
isUpdate?: boolean,
wasCurrentBinary?: boolean
) => Promise<boolean>;
setDownloading: (value: string | null) => void; setDownloading: (value: string | null) => void;
setDownloadProgress: ( setDownloadProgress: (
value: value:
@ -310,12 +312,12 @@ export const useKoboldVersions = (): UseKoboldVersionsReturn => {
}, [platformInfo.platform]); }, [platformInfo.platform]);
const handleDownload = useCallback( const handleDownload = useCallback(
async ( async ({
type: 'asset' | 'rocm', type,
item?: DownloadItem, item,
isUpdate = false, isUpdate = false,
wasCurrentBinary = false wasCurrentBinary = false,
): Promise<boolean> => { }: HandleDownloadParams): Promise<boolean> => {
if (type === 'asset' && !item) return false; if (type === 'asset' && !item) return false;
const downloadName = item?.name || 'download'; const downloadName = item?.name || 'download';

View file

@ -269,9 +269,10 @@ export const useLaunchLogic = ({
onLaunchModeChange(isImageMode); onLaunchModeChange(isImageMode);
} }
} else { } else {
const errorMessage = result.error || 'Unknown launch error';
window.electronAPI.logs.logError( window.electronAPI.logs.logError(
'Launch failed:', 'Launch failed:',
new Error(result.error) new Error(errorMessage)
); );
} }
} catch (error) { } catch (error) {

View file

@ -5,7 +5,6 @@ import type { LogManager } from '@/main/managers/LogManager';
import type { SillyTavernManager } from '@/main/managers/SillyTavernManager'; import type { SillyTavernManager } from '@/main/managers/SillyTavernManager';
import { HardwareService } from '@/main/services/HardwareService'; import { HardwareService } from '@/main/services/HardwareService';
import { BinaryService } from '@/main/services/BinaryService'; import { BinaryService } from '@/main/services/BinaryService';
import type { FrontendPreference } from '@/types';
export class IPCHandlers { export class IPCHandlers {
private koboldManager: KoboldCppManager; private koboldManager: KoboldCppManager;
@ -39,27 +38,14 @@ export class IPCHandlers {
error?: string; error?: string;
}> { }> {
try { try {
const finalArgs = [...args]; const frontendPreference =
const frontendPreference = (await this.configManager.get( await this.configManager.get('frontendPreference');
'frontendPreference'
)) as FrontendPreference | undefined;
if (frontendPreference !== 'koboldcpp' && !finalArgs.includes('--cli')) {
finalArgs.push('--cli');
}
if (frontendPreference === 'sillytavern') { if (frontendPreference === 'sillytavern') {
try { await this.sillyTavernManager.startFrontend(args);
await this.sillyTavernManager.startFrontend(finalArgs);
} catch (error) {
this.logManager.logError(
'Failed to setup SillyTavern:',
error as Error
);
}
} }
return await this.koboldManager.launchKoboldCpp(finalArgs); return await this.koboldManager.launchKoboldCpp(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 {
@ -147,7 +133,7 @@ export class IPCHandlers {
ipcMain.handle( ipcMain.handle(
'kobold:getAvailableBackends', 'kobold:getAvailableBackends',
(_event, includeDisabled = false) => (_, includeDisabled = false) =>
this.binaryService.getAvailableBackends(includeDisabled) this.binaryService.getAvailableBackends(includeDisabled)
); );
@ -176,9 +162,15 @@ export class IPCHandlers {
this.launchKoboldCppWithCustomFrontends(args) this.launchKoboldCppWithCustomFrontends(args)
); );
ipcMain.handle('kobold:stopKoboldCpp', () => ipcMain.handle('kobold:stopKoboldCpp', async () => {
this.koboldManager.stopKoboldCpp() try {
); this.koboldManager.stopKoboldCpp();
await this.sillyTavernManager.cleanup();
} catch (error) {
this.logManager.logError('Error during stop/cleanup:', error as Error);
throw error;
}
});
ipcMain.handle('kobold:parseConfigFile', (_event, filePath) => ipcMain.handle('kobold:parseConfigFile', (_event, filePath) =>
this.koboldManager.parseConfigFile(filePath) this.koboldManager.parseConfigFile(filePath)

View file

@ -61,7 +61,8 @@ export class ConfigManager {
} }
getCurrentKoboldBinary(): string | undefined { getCurrentKoboldBinary(): string | undefined {
return this.config.currentKoboldBinary as string | undefined; const path = this.config.currentKoboldBinary as string | undefined;
return path ? path.trim() : path;
} }
setCurrentKoboldBinary(binaryPath: string) { setCurrentKoboldBinary(binaryPath: string) {

View file

@ -1,5 +1,5 @@
/* eslint-disable no-comments/disallowComments */ /* eslint-disable no-comments/disallowComments */
import { spawn, ChildProcess, exec as execCmd } from 'child_process'; import { spawn, ChildProcess } from 'child_process';
import { join } from 'path'; import { join } from 'path';
import { import {
existsSync, existsSync,
@ -13,6 +13,8 @@ import {
} from 'fs'; } from 'fs';
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 { got } from 'got'; import { got } from 'got';
import { pipeline } from 'stream/promises'; import { pipeline } from 'stream/promises';
@ -46,6 +48,37 @@ export class KoboldCppManager {
this.installDir = this.configManager.getInstallDir() || ''; this.installDir = this.configManager.getInstallDir() || '';
} }
private async removeDirectoryWithRetry(
dirPath: string,
maxRetries = 3,
delayMs = 1000
): Promise<void> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await rm(dirPath, { recursive: true, force: true });
return;
} catch (error) {
const isLastAttempt = attempt === maxRetries;
const isPermissionError =
(error as Error & { code?: string }).code === 'EPERM';
if (isLastAttempt) {
throw error;
}
if (isPermissionError && process.platform === 'win32') {
this.windowManager.sendKoboldOutput(
`Attempt ${attempt}/${maxRetries} failed (file in use), retrying in ${delayMs}ms...`
);
await new Promise((resolve) => setTimeout(resolve, delayMs));
delayMs *= 1.5;
} else {
throw error;
}
}
}
}
async downloadRelease( async downloadRelease(
asset: GitHubAsset, asset: GitHubAsset,
onProgress?: (progress: number) => void onProgress?: (progress: number) => void
@ -56,12 +89,26 @@ export class KoboldCppManager {
if (asset.isUpdate && existsSync(unpackedDirPath)) { if (asset.isUpdate && existsSync(unpackedDirPath)) {
try { try {
await rm(unpackedDirPath, { recursive: true, force: true }); 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) { } catch (error) {
this.logManager.logError( this.logManager.logError(
'Failed to remove existing directory for update:', 'Failed to remove existing directory for update:',
error as Error 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}`
);
} }
} }
@ -698,10 +745,15 @@ export class KoboldCppManager {
const currentVersion = await this.getCurrentVersion(); const currentVersion = await this.getCurrentVersion();
if (!currentVersion || !existsSync(currentVersion.path)) { if (!currentVersion || !existsSync(currentVersion.path)) {
const error = 'KoboldCpp not found'; const rawPath = this.configManager.getCurrentKoboldBinary();
const error = currentVersion
? `KoboldCpp binary file does not exist at path: ${currentVersion.path}`
: 'No KoboldCpp version configured';
this.logManager.logError( this.logManager.logError(
`Launch failed: ${error}. Current version: ${JSON.stringify(currentVersion)}` `Launch failed: ${error}. Raw config path: "${rawPath}", Current version: ${JSON.stringify(currentVersion)}`
); );
return { return {
success: false, success: false,
error, error,
@ -801,75 +853,10 @@ export class KoboldCppManager {
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
if (this.koboldProcess) { if (this.koboldProcess) {
await new Promise<void>((resolve) => { await terminateProcess(this.koboldProcess, {
if (!this.koboldProcess) { logError: (message, error) => this.logManager.logError(message, error),
resolve();
return;
}
const cleanup = () => {
this.koboldProcess = null;
resolve();
};
this.koboldProcess.once('exit', cleanup);
this.koboldProcess.once('error', cleanup);
try {
if (process.platform === 'win32') {
const pid = this.koboldProcess.pid;
if (pid) {
try {
this.koboldProcess.kill('SIGTERM');
setTimeout(() => {
if (this.koboldProcess && !this.koboldProcess.killed) {
execCmd(
`taskkill /pid ${pid} /t /f`,
(error: Error | null) => {
if (error) {
this.logManager.logError(
'Error force-killing process:',
error
);
}
cleanup();
}
);
} else {
cleanup();
}
}, 2000);
} catch (error) {
this.logManager.logError(
'Error during Windows cleanup:',
error as Error
);
cleanup();
}
} else {
cleanup();
}
} else {
// Unix-like systems
this.koboldProcess.kill('SIGTERM');
setTimeout(() => {
if (this.koboldProcess && !this.koboldProcess.killed) {
try {
this.koboldProcess.kill('SIGKILL');
} catch {
void 0;
}
}
cleanup();
}, 3000);
}
} catch (error) {
this.logManager.logError('Error during cleanup:', error as Error);
cleanup();
}
}); });
this.koboldProcess = null;
} }
} }
} }

View file

@ -44,6 +44,20 @@ export class LogManager {
public logError(message: string, error?: Error) { public logError(message: string, error?: Error) {
this.logger.error(message, { error }); this.logger.error(message, { error });
this.flush();
}
public flush() {
const fileTransport = this.logger.transports.find(
(t) => t.constructor.name === 'DailyRotateFile'
);
if (
fileTransport &&
'flush' in fileTransport &&
typeof fileTransport.flush === 'function'
) {
(fileTransport as { flush: () => void }).flush();
}
} }
public logDebug(message: string) { public logDebug(message: string) {

View file

@ -8,6 +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';
export interface SillyTavernConfig { export interface SillyTavernConfig {
name: string; name: string;
@ -38,53 +39,83 @@ export class SillyTavernManager {
}); });
} }
private getSillyTavernSettingsPath(): string { private detectedDataRoot: string | null = null;
private getFallbackDataRoot(): string {
const platform = process.platform; const platform = process.platform;
const home = homedir(); const home = homedir();
switch (platform) { switch (platform) {
case 'win32': case 'win32':
return join( return join(home, 'AppData', 'Local', 'SillyTavern', 'Data', 'data');
home,
'AppData',
'Roaming',
'SillyTavern',
'data',
'default-user',
'settings.json'
);
case 'darwin': case 'darwin':
return join( return join(
home, home,
'Library', 'Library',
'Application Support', 'Application Support',
'SillyTavern', 'SillyTavern',
'data', 'data'
'default-user',
'settings.json'
); );
case 'linux': case 'linux':
default: default:
return join( return join(home, '.local', 'share', 'SillyTavern', 'data');
home,
'.local',
'share',
'SillyTavern',
'data',
'default-user',
'settings.json'
);
} }
} }
private getSillyTavernDataRoot(): string {
if (this.detectedDataRoot) {
return this.detectedDataRoot;
}
const fallback = this.getFallbackDataRoot();
if (existsSync(fallback)) {
this.detectedDataRoot = fallback;
return fallback;
}
const platform = process.platform;
const home = homedir();
const alternatePaths = [];
switch (platform) {
case 'win32':
alternatePaths.push(
join(home, 'AppData', 'Local', 'SillyTavern', 'data'),
join(home, '.sillytavern', 'data')
);
break;
case 'darwin':
alternatePaths.push(join(home, '.sillytavern', 'data'));
break;
case 'linux':
default:
alternatePaths.push(join(home, '.sillytavern', 'data'));
break;
}
for (const path of alternatePaths) {
if (existsSync(path)) {
this.detectedDataRoot = path;
return path;
}
}
this.detectedDataRoot = fallback;
return fallback;
}
private getSillyTavernSettingsPath(): string {
return join(this.getSillyTavernDataRoot(), 'default-user', 'settings.json');
}
private getSillyTavernBaseArgs(): string[] { private getSillyTavernBaseArgs(): string[] {
return [ return [
'sillytavern', 'sillytavern',
'--listen', '--listen',
'--browserLaunchEnabled', '--browserLaunchEnabled',
'false', 'false',
'--securityOverride', '--disableCsrf',
'true',
]; ];
} }
@ -117,6 +148,10 @@ export class SillyTavernManager {
return spawn('npx', args, { return spawn('npx', args, {
stdio: ['pipe', 'pipe', 'pipe'], stdio: ['pipe', 'pipe', 'pipe'],
detached: false, detached: false,
env: {
...process.env,
DATA_ROOT: this.getSillyTavernDataRoot(),
},
}); });
} }
@ -143,35 +178,37 @@ export class SillyTavernManager {
let hasResolved = false; let hasResolved = false;
const cleanupAndResolve = () => {
if (!hasResolved) {
hasResolved = true;
this.windowManager.sendKoboldOutput(
'SillyTavern settings should now be generated'
);
resolve();
}
};
const timeout = setTimeout(() => {
if (!initProcess.killed) {
initProcess.kill('SIGTERM');
}
cleanupAndResolve();
}, 90000);
initProcess.on('exit', (code: number | null, signal: string | null) => { initProcess.on('exit', (code: number | null, signal: string | null) => {
clearTimeout(timeout);
const exitMessage = signal const exitMessage = signal
? `SillyTavern init process terminated with signal ${signal}` ? `SillyTavern init process terminated with signal ${signal}`
: `SillyTavern init process exited with code ${code}`; : `SillyTavern init process exited with code ${code}`;
this.windowManager.sendKoboldOutput(exitMessage); this.windowManager.sendKoboldOutput(exitMessage);
cleanupAndResolve();
if (!hasResolved) {
hasResolved = true;
if (code !== 0) {
const errorMsg =
code === 4294963214
? 'SillyTavern failed to install due to EBUSY error (resource busy or locked). This is a critical error.'
: `SillyTavern initialization failed with exit code ${code}`;
this.logManager.logError(
'SillyTavern initialization failed:',
new Error(errorMsg)
);
this.windowManager.sendKoboldOutput(`CRITICAL ERROR: ${errorMsg}`);
reject(new Error(errorMsg));
} else {
this.windowManager.sendKoboldOutput(
'SillyTavern settings should now be generated'
);
resolve();
}
}
}); });
initProcess.on('error', (error) => { initProcess.on('error', (error) => {
clearTimeout(timeout);
if (!hasResolved) { if (!hasResolved) {
hasResolved = true; hasResolved = true;
this.logManager.logError( this.logManager.logError(
@ -192,8 +229,12 @@ export class SillyTavernManager {
if (output.includes('SillyTavern is listening')) { if (output.includes('SillyTavern is listening')) {
setTimeout(() => { setTimeout(() => {
if (!initProcess.killed && !hasResolved) { if (!initProcess.killed && !hasResolved) {
hasResolved = true;
initProcess.kill('SIGTERM'); initProcess.kill('SIGTERM');
cleanupAndResolve(); this.windowManager.sendKoboldOutput(
'SillyTavern settings should now be generated'
);
resolve();
} }
}, 1000); }, 1000);
} }
@ -206,14 +247,6 @@ export class SillyTavernManager {
this.windowManager.sendKoboldOutput(output.trim()); this.windowManager.sendKoboldOutput(output.trim());
}); });
} }
setTimeout(() => {
if (!initProcess.killed && !hasResolved) {
this.windowManager.sendKoboldOutput(
'SillyTavern initialization taking longer than expected, please wait...'
);
}
}, 30000);
}); });
} }
@ -277,7 +310,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 = koboldUrl; serverUrls.koboldcpp = koboldApiUrl;
settings.main_api = 'textgenerationwebui'; settings.main_api = 'textgenerationwebui';
textgenSettings.type = 'koboldcpp'; textgenSettings.type = 'koboldcpp';
@ -288,9 +321,6 @@ export class SillyTavernManager {
this.windowManager.sendKoboldOutput( this.windowManager.sendKoboldOutput(
`SillyTavern configuration updated successfully!` `SillyTavern configuration updated successfully!`
); );
this.windowManager.sendKoboldOutput(
`KoboldCpp will auto-connect to: ${koboldApiUrl}`
);
} catch (error) { } catch (error) {
this.logManager.logError( this.logManager.logError(
'Failed to setup SillyTavern config:', 'Failed to setup SillyTavern config:',
@ -302,6 +332,39 @@ export class SillyTavernManager {
} }
} }
private async waitForSillyTavernToStart(_port: number): Promise<void> {
this.windowManager.sendKoboldOutput('Waiting for SillyTavern to start...');
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('SillyTavern failed to start within 30 seconds'));
}, 30000);
const checkForOutput = (data: Buffer) => {
const output = data.toString();
if (output.includes('SillyTavern is listening')) {
clearTimeout(timeout);
this.windowManager.sendKoboldOutput('SillyTavern is now running!');
resolve();
if (this.sillyTavernProcess?.stdout) {
this.sillyTavernProcess.stdout.removeListener(
'data',
checkForOutput
);
}
}
};
if (this.sillyTavernProcess?.stdout) {
this.sillyTavernProcess.stdout.on('data', checkForOutput);
} else {
clearTimeout(timeout);
reject(new Error('SillyTavern process stdout not available'));
}
});
}
private createProxyServer(targetPort: number, proxyPort: number): void { private createProxyServer(targetPort: number, proxyPort: number): void {
this.proxyServer = createServer((req, res) => { this.proxyServer = createServer((req, res) => {
const options = { const options = {
@ -351,6 +414,8 @@ 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}...`
); );
@ -368,6 +433,11 @@ export class SillyTavernManager {
config.port.toString(), config.port.toString(),
]; ];
this.windowManager.sendKoboldOutput(
'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) {
@ -403,16 +473,8 @@ export class SillyTavernManager {
this.sillyTavernProcess = null; this.sillyTavernProcess = null;
}); });
if (config.proxyPort) { await this.waitForSillyTavernToStart(config.port);
this.createProxyServer(config.port, config.proxyPort); this.createProxyServer(config.port, config.proxyPort);
this.windowManager.sendKoboldOutput(
`SillyTavern proxy starting at http://localhost:${config.proxyPort} (forwarding to port ${config.port})`
);
} else {
this.windowManager.sendKoboldOutput(
`SillyTavern starting at http://localhost:${config.port}`
);
}
} catch (error) { } catch (error) {
this.logManager.logError( this.logManager.logError(
`Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`, `Failed to start SillyTavern: ${error instanceof Error ? error.message : String(error)}`,
@ -423,26 +485,33 @@ export class SillyTavernManager {
} }
async stopFrontend(): Promise<void> { async stopFrontend(): Promise<void> {
this.windowManager.sendKoboldOutput('Stopping SillyTavern frontend...');
try {
this.windowManager.sendKoboldOutput(
`Killing processes on port ${SILLYTAVERN.PORT}...`
);
await killProcessOnPort(SILLYTAVERN.PORT);
this.windowManager.sendKoboldOutput(
`Port ${SILLYTAVERN.PORT} is now free`
);
} catch (error) {
this.logManager.logError(
'Error killing processes on port:',
error as Error
);
}
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
if (this.sillyTavernProcess) { if (this.sillyTavernProcess) {
promises.push( promises.push(
new Promise((resolve) => { terminateProcess(this.sillyTavernProcess, {
const timeout = setTimeout(() => { logError: (message, error) =>
this.logManager.logError( this.logManager.logError(message, error),
'SillyTavern did not close within timeout', }).then(() => {
new Error('Process close timeout') this.sillyTavernProcess = null;
); this.windowManager.sendKoboldOutput('SillyTavern process terminated');
this.sillyTavernProcess = null;
resolve();
}, 5000);
this.sillyTavernProcess?.kill('SIGTERM');
this.sillyTavernProcess?.once('exit', () => {
clearTimeout(timeout);
this.sillyTavernProcess = null;
resolve();
});
}) })
); );
} }
@ -452,6 +521,7 @@ export class SillyTavernManager {
new Promise((resolve) => { new Promise((resolve) => {
this.proxyServer?.close(() => { this.proxyServer?.close(() => {
this.proxyServer = null; this.proxyServer = null;
this.windowManager.sendKoboldOutput('Proxy server closed');
resolve(); resolve();
}); });
}) })
@ -459,8 +529,8 @@ export class SillyTavernManager {
} }
await Promise.all(promises); await Promise.all(promises);
this.windowManager.sendKoboldOutput('SillyTavern frontend stopped');
} }
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
if (this.sillyTavernProcess) { if (this.sillyTavernProcess) {
try { try {

View file

@ -11,7 +11,8 @@ export class WindowManager {
private getIconPath(): string { private getIconPath(): string {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
return join(__dirname, '../../assets/icon.png'); const projectRoot = join(__dirname, '../..');
return join(projectRoot, 'src/assets/icon.png');
} }
return join(process.resourcesPath, 'assets/icon.png'); return join(process.resourcesPath, 'assets/icon.png');
} }

100
src/utils/processUtils.ts Normal file
View file

@ -0,0 +1,100 @@
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);
}
}

220
yarn.lock
View file

@ -1601,6 +1601,16 @@ __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"
@ -1946,16 +1956,16 @@ __metadata:
linkType: hard linkType: hard
"browserslist@npm:^4.24.0": "browserslist@npm:^4.24.0":
version: 4.25.3 version: 4.25.4
resolution: "browserslist@npm:4.25.3" resolution: "browserslist@npm:4.25.4"
dependencies: dependencies:
caniuse-lite: "npm:^1.0.30001735" caniuse-lite: "npm:^1.0.30001737"
electron-to-chromium: "npm:^1.5.204" electron-to-chromium: "npm:^1.5.211"
node-releases: "npm:^2.0.19" node-releases: "npm:^2.0.19"
update-browserslist-db: "npm:^1.1.3" update-browserslist-db: "npm:^1.1.3"
bin: bin:
browserslist: cli.js browserslist: cli.js
checksum: 10c0/cefbbf962b7c0f0d77e952a4b4b37469db7f7f02bc2be7297810ac3cf086660f48daf78d00f7aad8a11b682f88b0ee0022594165ead749e9e4158a0aa49cd161 checksum: 10c0/2b105948990dc2fc0bc2536b4889aadfa15d637e1d857a121611a704cdf539a68f575a391f6bf8b7ff19db36cee1b7834565571f35a7ea691051d2e7fb4f2eb1
languageName: node languageName: node
linkType: hard linkType: hard
@ -2168,7 +2178,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"caniuse-lite@npm:^1.0.30001735": "caniuse-lite@npm:^1.0.30001737":
version: 1.0.30001737 version: 1.0.30001737
resolution: "caniuse-lite@npm:1.0.30001737" resolution: "caniuse-lite@npm:1.0.30001737"
checksum: 10c0/9d9cfe3b46fe670d171cee10c5c1b0fb641946fd5d6bea26149f804003d53d82ade7ef5a4a640fb3a0eaec47c7839b57e06a6ddae4f0ad2cd58e1187d31997ce checksum: 10c0/9d9cfe3b46fe670d171cee10c5c1b0fb641946fd5d6bea26149f804003d53d82ade7ef5a4a640fb3a0eaec47c7839b57e06a6ddae4f0ad2cd58e1187d31997ce
@ -2227,6 +2237,15 @@ __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"
@ -2448,7 +2467,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.6": "cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.3, 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:
@ -2745,7 +2764,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"electron-to-chromium@npm:^1.5.204": "electron-to-chromium@npm:^1.5.211":
version: 1.5.211 version: 1.5.211
resolution: "electron-to-chromium@npm:1.5.211" resolution: "electron-to-chromium@npm:1.5.211"
checksum: 10c0/587536f2e319b7484cd4c9e83484f461ee06672c588c84bf4d4b6a6b5d00fbdb621d4ca418a68125a86db95d373b890b47de2fb5a0f52592cc8aebc263623e6e checksum: 10c0/587536f2e319b7484cd4c9e83484f461ee06672c588c84bf4d4b6a6b5d00fbdb621d4ca418a68125a86db95d373b890b47de2fb5a0f52592cc8aebc263623e6e
@ -2788,9 +2807,9 @@ __metadata:
linkType: hard linkType: hard
"emoji-regex@npm:^10.3.0": "emoji-regex@npm:^10.3.0":
version: 10.4.0 version: 10.5.0
resolution: "emoji-regex@npm:10.4.0" resolution: "emoji-regex@npm:10.5.0"
checksum: 10c0/a3fcedfc58bfcce21a05a5f36a529d81e88d602100145fcca3dc6f795e3c8acc4fc18fe773fbf9b6d6e9371205edb3afa2668ec3473fa2aa7fd47d2a9d46482d checksum: 10c0/17cf84335a461fc23bf90575122ace2902630dc760e53299474cd3b0b5e4cfbc6c0223a389a766817538e5d20bf0f36c67b753f27c9e705056af510b8777e312
languageName: node languageName: node
linkType: hard linkType: hard
@ -3098,6 +3117,13 @@ __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"
@ -3354,6 +3380,40 @@ __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"
@ -3531,6 +3591,20 @@ __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"
@ -3619,6 +3693,7 @@ __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"
@ -3827,6 +3902,20 @@ __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"
@ -4140,6 +4229,20 @@ __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"
@ -4229,6 +4332,13 @@ __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"
@ -4514,6 +4624,13 @@ __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"
@ -4615,9 +4732,9 @@ __metadata:
linkType: hard linkType: hard
"isbinaryfile@npm:^5.0.0": "isbinaryfile@npm:^5.0.0":
version: 5.0.5 version: 5.0.6
resolution: "isbinaryfile@npm:5.0.5" resolution: "isbinaryfile@npm:5.0.6"
checksum: 10c0/c90e57fec90f44c582e57512d7ffaadaaecd9def1f297f7d2a97469c42cde7dc6aecf9b812a455b83a4a8c7eab455c50afb685d3bf7971ddaed365a7c6c81b78 checksum: 10c0/53d700189a2a97965f2dcc9ced47644daad24b68ca42eb8a170f0cc0f2b83d52d58d956d969f0dd5f0d4f0012dc488dba079cacd7086c595932897da71085a7f
languageName: node languageName: node
linkType: hard linkType: hard
@ -5071,6 +5188,13 @@ __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"
@ -5120,6 +5244,13 @@ __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"
@ -5467,6 +5598,15 @@ __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"
@ -5593,6 +5733,15 @@ __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"
@ -5806,6 +5955,15 @@ __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"
@ -5883,6 +6041,15 @@ __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"
@ -5918,6 +6085,13 @@ __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"
@ -6631,7 +6805,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"signal-exit@npm:^3.0.2": "signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.7":
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
@ -6956,6 +7130,13 @@ __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"
@ -7040,6 +7221,15 @@ __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"