diff --git a/src/main/cli.ts b/src/main/cli.ts new file mode 100644 index 0000000..86da7f7 --- /dev/null +++ b/src/main/cli.ts @@ -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 { + 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((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); + } + }); + } +} diff --git a/src/main/gui.ts b/src/main/gui.ts new file mode 100644 index 0000000..c5c7a82 --- /dev/null +++ b/src/main/gui.ts @@ -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 { + 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((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(); + } + }); + } +} diff --git a/src/main/index.ts b/src/main/index.ts index 92462e7..6b5b4b6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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 { - 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((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); + }); }); } diff --git a/src/main/utils/CliHandler.ts b/src/main/utils/CliHandler.ts index 1a297bf..fd8c008 100644 --- a/src/main/utils/CliHandler.ts +++ b/src/main/utils/CliHandler.ts @@ -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 { - const currentBinary = this.configManager.getCurrentKoboldBinary(); + const currentBinary = this.getCurrentKoboldBinary(); if (!currentBinary) { console.error(