further separate gui and cli for slightly less ram usage in CLI mode

This commit is contained in:
Egor 2025-08-24 18:06:38 -07:00
parent 458e083da4
commit 09e6155476
4 changed files with 334 additions and 178 deletions

127
src/main/cli.ts Normal file
View file

@ -0,0 +1,127 @@
/* eslint-disable no-console */
import { spawn } from 'child_process';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
const CONFIG_FILE_NAME = 'config.json';
export class LightweightCliHandler {
private getConfigPath(): string {
const platform = process.platform;
const home = homedir();
switch (platform) {
case 'win32':
return join(
home,
'AppData',
'Roaming',
'Friendly Kobold',
CONFIG_FILE_NAME
);
case 'darwin':
return join(
home,
'Library',
'Application Support',
'Friendly Kobold',
CONFIG_FILE_NAME
);
default:
return join(home, '.config', 'Friendly Kobold', CONFIG_FILE_NAME);
}
}
private getCurrentKoboldBinary(): string | null {
try {
const configPath = this.getConfigPath();
if (!existsSync(configPath)) {
return null;
}
const config = JSON.parse(readFileSync(configPath, 'utf8'));
return config.currentKoboldBinary || null;
} catch {
return null;
}
}
async handleCliMode(args: string[]): Promise<void> {
const currentBinary = this.getCurrentKoboldBinary();
if (!currentBinary) {
console.error(
'Error: No KoboldCpp binary found. Please run the GUI first to download KoboldCpp.'
);
process.exit(1);
}
if (!existsSync(currentBinary)) {
console.error(`Error: KoboldCpp binary not found at: ${currentBinary}`);
console.error('Please run the GUI to download or reconfigure KoboldCpp.');
process.exit(1);
}
return new Promise<void>((resolve, reject) => {
const isWindows = process.platform === 'win32';
const child = spawn(currentBinary, args, {
stdio: isWindows ? 'pipe' : 'inherit',
detached: false,
});
if (isWindows) {
child.stdout?.setEncoding('utf8');
child.stderr?.setEncoding('utf8');
child.stdout?.on('data', (data) => {
process.stdout.write(data.toString());
});
child.stderr?.on('data', (data) => {
process.stderr.write(data.toString());
});
if (child.stdin && process.stdin.readable) {
process.stdin.pipe(child.stdin);
}
}
child.on('exit', (code, signal) => {
if (signal) {
console.log(`\nProcess terminated with signal: ${signal}`);
process.exit(128 + (signal === 'SIGTERM' ? 15 : 2));
} else if (code !== null) {
process.exit(code);
} else {
resolve();
}
});
child.on('error', (error) => {
console.error(`Failed to start KoboldCpp: ${error.message}`);
reject(error);
});
const handleSignal = () => {
console.log('\nReceived termination signal, terminating KoboldCpp...');
if (!child.killed) {
child.kill('SIGTERM');
setTimeout(() => {
if (!child.killed) {
child.kill('SIGKILL');
}
}, 5000);
}
};
process.on('SIGINT', handleSignal);
process.on('SIGTERM', handleSignal);
if (process.platform === 'win32') {
process.on('SIGBREAK', handleSignal);
}
});
}
}

147
src/main/gui.ts Normal file
View file

@ -0,0 +1,147 @@
import { app } from 'electron';
import { join } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { homedir } from 'os';
import { WindowManager } from '@/main/managers/WindowManager';
import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager';
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
import { GitHubService } from '@/main/services/GitHubService';
import { HardwareService } from '@/main/services/HardwareService';
import { BinaryService } from '@/main/services/BinaryService';
import { IPCHandlers } from '@/main/utils/IPCHandlers';
import { APP_NAME, CONFIG_FILE_NAME } from '@/constants';
export class FriendlyKoboldApp {
private windowManager: WindowManager;
private configManager: ConfigManager;
private logManager: LogManager;
private koboldManager: KoboldCppManager;
private githubService: GitHubService;
private hardwareService: HardwareService;
private binaryService: BinaryService;
private ipcHandlers: IPCHandlers;
constructor() {
this.logManager = new LogManager();
this.logManager.setupGlobalErrorHandlers();
this.configManager = new ConfigManager(
this.getConfigPath(),
this.logManager
);
this.ensureInstallDirectory();
this.windowManager = new WindowManager();
this.githubService = new GitHubService(this.logManager);
this.hardwareService = new HardwareService(this.logManager);
this.koboldManager = new KoboldCppManager(
this.configManager,
this.githubService,
this.windowManager,
this.logManager
);
this.binaryService = new BinaryService(
this.logManager,
this.koboldManager,
this.hardwareService
);
this.ipcHandlers = new IPCHandlers(
this.koboldManager,
this.configManager,
this.githubService,
this.hardwareService,
this.binaryService,
this.logManager
);
}
private getConfigPath() {
return join(app.getPath('userData'), CONFIG_FILE_NAME);
}
private getDefaultInstallPath() {
const platform = process.platform;
const home = homedir();
switch (platform) {
case 'win32':
return join(home, APP_NAME);
case 'darwin':
return join(home, 'Applications', APP_NAME);
default:
return join(home, '.local', 'share', APP_NAME);
}
}
private ensureInstallDirectory() {
const installDir =
this.configManager.getInstallDir() || this.getDefaultInstallPath();
if (!this.configManager.getInstallDir()) {
this.configManager.setInstallDir(installDir);
}
if (!existsSync(installDir)) {
mkdirSync(installDir, { recursive: true });
}
}
async initialize(): Promise<void> {
await app.whenReady();
if (process.platform === 'linux') {
app.setAppUserModelId('com.friendly-kobold.app');
}
this.windowManager.setupApplicationMenu();
this.windowManager.createMainWindow();
this.ipcHandlers.setupHandlers();
app.on('window-all-closed', () => {
if (process.platform === 'darwin') {
return;
}
app.quit();
});
app.on('before-quit', async (event) => {
event.preventDefault();
try {
const cleanupPromise = this.koboldManager.cleanup();
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 10000);
});
await Promise.race([cleanupPromise, timeoutPromise]);
} catch (error) {
this.logManager.logError(
'Error during KoboldCpp cleanup:',
error as Error
);
}
this.windowManager.cleanup();
app.exit(0);
});
app.on('will-quit', async (event) => {
event.preventDefault();
app.exit(0);
});
app.on('activate', () => {
if (!this.windowManager.getMainWindow()) {
this.windowManager.createMainWindow();
}
});
}
}

View file

@ -1,170 +1,29 @@
import { app } from 'electron';
import { join } from 'path';
import { existsSync, mkdirSync } from 'fs';
import { homedir } from 'os';
import { WindowManager } from '@/main/managers/WindowManager';
import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager';
import { KoboldCppManager } from '@/main/managers/KoboldCppManager';
import { GitHubService } from '@/main/services/GitHubService';
import { HardwareService } from '@/main/services/HardwareService';
import { BinaryService } from '@/main/services/BinaryService';
import { IPCHandlers } from '@/main/utils/IPCHandlers';
import { CliHandler } from '@/main/utils/CliHandler';
import { APP_NAME, CONFIG_FILE_NAME } from '@/constants';
class FriendlyKoboldApp {
private windowManager: WindowManager;
private configManager: ConfigManager;
private logManager: LogManager;
private koboldManager: KoboldCppManager;
private githubService: GitHubService;
private hardwareService: HardwareService;
private binaryService: BinaryService;
private ipcHandlers: IPCHandlers;
constructor() {
this.logManager = new LogManager();
this.logManager.setupGlobalErrorHandlers();
this.configManager = new ConfigManager(
this.getConfigPath(),
this.logManager
);
this.ensureInstallDirectory();
this.windowManager = new WindowManager();
this.githubService = new GitHubService(this.logManager);
this.hardwareService = new HardwareService(this.logManager);
this.koboldManager = new KoboldCppManager(
this.configManager,
this.githubService,
this.windowManager,
this.logManager
);
this.binaryService = new BinaryService(
this.logManager,
this.koboldManager,
this.hardwareService
);
this.ipcHandlers = new IPCHandlers(
this.koboldManager,
this.configManager,
this.githubService,
this.hardwareService,
this.binaryService,
this.logManager
);
}
private getConfigPath() {
return join(app.getPath('userData'), CONFIG_FILE_NAME);
}
private getDefaultInstallPath() {
const platform = process.platform;
const home = homedir();
switch (platform) {
case 'win32':
return join(home, APP_NAME);
case 'darwin':
return join(home, 'Applications', APP_NAME);
default:
return join(home, '.local', 'share', APP_NAME);
}
}
private ensureInstallDirectory() {
const installDir =
this.configManager.getInstallDir() || this.getDefaultInstallPath();
if (!this.configManager.getInstallDir()) {
this.configManager.setInstallDir(installDir);
}
if (!existsSync(installDir)) {
mkdirSync(installDir, { recursive: true });
}
}
async initialize(): Promise<void> {
await app.whenReady();
if (process.platform === 'linux') {
app.setAppUserModelId('com.friendly-kobold.app');
}
this.windowManager.setupApplicationMenu();
this.windowManager.createMainWindow();
this.ipcHandlers.setupHandlers();
app.on('window-all-closed', () => {
if (process.platform === 'darwin') {
return;
}
app.quit();
});
app.on('before-quit', async (event) => {
event.preventDefault();
try {
const cleanupPromise = this.koboldManager.cleanup();
const timeoutPromise = new Promise<void>((resolve) => {
setTimeout(() => {
resolve();
}, 10000);
});
await Promise.race([cleanupPromise, timeoutPromise]);
} catch (error) {
this.logManager.logError(
'Error during KoboldCpp cleanup:',
error as Error
);
}
this.windowManager.cleanup();
app.exit(0);
});
app.on('will-quit', async (event) => {
event.preventDefault();
app.exit(0);
});
app.on('activate', () => {
if (!this.windowManager.getMainWindow()) {
this.windowManager.createMainWindow();
}
});
}
}
const { isCliMode, args } = CliHandler.parseArguments(process.argv);
const isCliMode = process.argv.includes('--cli');
if (isCliMode) {
async function runCliMode() {
try {
const cliHandler = new CliHandler();
await cliHandler.handleCliMode(args);
} catch (error) {
import('./cli')
.then(async (cliModule) => {
const args = process.argv.slice(process.argv.indexOf('--cli') + 1);
const handler = new cliModule.LightweightCliHandler();
try {
await handler.handleCliMode(args);
} catch (error) {
// eslint-disable-next-line no-console
console.error('CLI mode error:', error);
process.exit(1);
}
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error('CLI mode error:', error);
console.error('Failed to load CLI module:', error);
process.exit(1);
}
}
runCliMode();
});
} else {
const friendlyKoboldApp = new FriendlyKoboldApp();
friendlyKoboldApp.initialize().catch((error) => {
// eslint-disable-next-line no-console
console.error('Failed to initialize FriendlyKobold:', error);
import('./gui').then((guiModule) => {
const app = new guiModule.FriendlyKoboldApp();
app.initialize().catch((error: unknown) => {
// eslint-disable-next-line no-console
console.error('Failed to initialize FriendlyKobold:', error);
});
});
}

View file

@ -1,31 +1,54 @@
/* eslint-disable no-console */
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import { app } from 'electron';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
import { ConfigManager } from '@/main/managers/ConfigManager';
import { LogManager } from '@/main/managers/LogManager';
import { CONFIG_FILE_NAME } from '@/constants';
export class CliHandler {
private configManager: ConfigManager;
private logManager: LogManager;
private getConfigPath(): string {
const platform = process.platform;
const home = homedir();
constructor() {
this.logManager = new LogManager();
this.configManager = new ConfigManager(
this.getConfigPath(),
this.logManager
);
switch (platform) {
case 'win32':
return join(
home,
'AppData',
'Roaming',
'Friendly Kobold',
CONFIG_FILE_NAME
);
case 'darwin':
return join(
home,
'Library',
'Application Support',
'Friendly Kobold',
CONFIG_FILE_NAME
);
default:
return join(home, '.config', 'Friendly Kobold', CONFIG_FILE_NAME);
}
}
private getConfigPath(): string {
return join(app.getPath('userData'), CONFIG_FILE_NAME);
private getCurrentKoboldBinary(): string | null {
try {
const configPath = this.getConfigPath();
if (!existsSync(configPath)) {
return null;
}
const config = JSON.parse(readFileSync(configPath, 'utf8'));
return config.currentKoboldBinary || null;
} catch {
return null;
}
}
async handleCliMode(args: string[]): Promise<void> {
const currentBinary = this.configManager.getCurrentKoboldBinary();
const currentBinary = this.getCurrentKoboldBinary();
if (!currentBinary) {
console.error(