@@ -12,6 +12,8 @@ import { loadDatadogConfig, saveDatadogConfig, clearDatadogConfig, fetchDatadogL
1212import { startApiServer , stopApiServer , ApiContext } from './api-server' ;
1313import { SerialHandler } from './serialHandler' ;
1414import { LogcatHandler } from './logcatHandler' ;
15+ import { SshHandler } from './sshHandler' ;
16+ import { SshProfile } from '../shared/types' ;
1517
1618let mainWindow : BrowserWindow | null = null ;
1719let searchSignal : { cancelled : boolean } = { cancelled : false } ;
@@ -24,6 +26,9 @@ const serialHandler = new SerialHandler();
2426// Logcat handler
2527const logcatHandler = new LogcatHandler ( ) ;
2628
29+ // SSH handler
30+ const sshHandler = new SshHandler ( ) ;
31+
2732// Filter state - maps file path to array of visible line indices
2833const filterState = new Map < string , number [ ] | null > ( ) ;
2934
@@ -698,6 +703,7 @@ app.on('window-all-closed', () => {
698703app . on ( 'will-quit' , ( ) => {
699704 serialHandler . cleanupTempFile ( ) ;
700705 logcatHandler . cleanupTempFile ( ) ;
706+ sshHandler . cleanupTempFile ( ) ;
701707 stopApiServer ( ) ;
702708} ) ;
703709
@@ -916,6 +922,175 @@ ipcMain.handle(IPC.LOGCAT_SAVE_SESSION, async () => {
916922 }
917923} ) ;
918924
925+ // === SSH ===
926+
927+ const getSshProfilesPath = ( ) => path . join ( getConfigDir ( ) , 'ssh-profiles.json' ) ;
928+
929+ function loadSshProfiles ( ) : SshProfile [ ] {
930+ try {
931+ const p = getSshProfilesPath ( ) ;
932+ if ( fs . existsSync ( p ) ) {
933+ return JSON . parse ( fs . readFileSync ( p , 'utf-8' ) ) ;
934+ }
935+ } catch { /* */ }
936+ return [ ] ;
937+ }
938+
939+ function saveSshProfiles ( profiles : SshProfile [ ] ) : void {
940+ try {
941+ ensureConfigDir ( ) ;
942+ fs . writeFileSync ( getSshProfilesPath ( ) , JSON . stringify ( profiles , null , 2 ) , 'utf-8' ) ;
943+ } catch ( error ) {
944+ console . error ( 'Failed to save SSH profiles:' , error ) ;
945+ }
946+ }
947+
948+ ipcMain . handle ( IPC . SSH_PARSE_CONFIG , async ( ) => {
949+ try {
950+ const hosts = sshHandler . parseSSHConfig ( ) ;
951+ return { success : true , hosts } ;
952+ } catch ( error ) {
953+ return { success : false , error : String ( error ) } ;
954+ }
955+ } ) ;
956+
957+ ipcMain . handle ( IPC . SSH_LIST_PROFILES , async ( ) => {
958+ try {
959+ return { success : true , profiles : loadSshProfiles ( ) } ;
960+ } catch ( error ) {
961+ return { success : false , error : String ( error ) } ;
962+ }
963+ } ) ;
964+
965+ ipcMain . handle ( IPC . SSH_SAVE_PROFILE , async ( _ , profile : SshProfile ) => {
966+ try {
967+ const profiles = loadSshProfiles ( ) ;
968+ const idx = profiles . findIndex ( p => p . id === profile . id ) ;
969+ if ( idx >= 0 ) {
970+ profiles [ idx ] = profile ;
971+ } else {
972+ profiles . push ( profile ) ;
973+ }
974+ saveSshProfiles ( profiles ) ;
975+ return { success : true } ;
976+ } catch ( error ) {
977+ return { success : false , error : String ( error ) } ;
978+ }
979+ } ) ;
980+
981+ ipcMain . handle ( IPC . SSH_DELETE_PROFILE , async ( _ , id : string ) => {
982+ try {
983+ const profiles = loadSshProfiles ( ) . filter ( p => p . id !== id ) ;
984+ saveSshProfiles ( profiles ) ;
985+ return { success : true } ;
986+ } catch ( error ) {
987+ return { success : false , error : String ( error ) } ;
988+ }
989+ } ) ;
990+
991+ ipcMain . handle ( IPC . SSH_CONNECT , async ( _ , config : { host : string ; port : number ; username : string ; identityFile ?: string ; remotePath : string ; passphrase ?: string } ) => {
992+ try {
993+ const tempFilePath = await sshHandler . connect ( config ) ;
994+
995+ // Open temp file with FileHandler
996+ const fileHandler = new FileHandler ( ) ;
997+ const info = await fileHandler . open ( tempFilePath , ( ) => { } ) ;
998+ addToCache ( tempFilePath , fileHandler ) ;
999+ currentFilePath = tempFilePath ;
1000+
1001+ // Forward events to renderer
1002+ const onLinesAdded = ( count : number ) => {
1003+ const handler = fileHandlerCache . get ( tempFilePath ) ;
1004+ if ( handler ) {
1005+ const newLines = handler . indexNewLines ( ) ;
1006+ if ( newLines > 0 ) {
1007+ mainWindow ?. webContents . send ( IPC . SSH_LINES_ADDED , {
1008+ totalLines : handler . getTotalLines ( ) ,
1009+ newLines,
1010+ } ) ;
1011+ }
1012+ }
1013+ } ;
1014+
1015+ const onError = ( message : string ) => {
1016+ mainWindow ?. webContents . send ( IPC . SSH_ERROR , message ) ;
1017+ } ;
1018+
1019+ const onDisconnected = ( ) => {
1020+ mainWindow ?. webContents . send ( IPC . SSH_DISCONNECTED ) ;
1021+ sshHandler . removeListener ( 'lines-added' , onLinesAdded ) ;
1022+ sshHandler . removeListener ( 'error' , onError ) ;
1023+ sshHandler . removeListener ( 'disconnected' , onDisconnected ) ;
1024+ } ;
1025+
1026+ sshHandler . on ( 'lines-added' , onLinesAdded ) ;
1027+ sshHandler . on ( 'error' , onError ) ;
1028+ sshHandler . on ( 'disconnected' , onDisconnected ) ;
1029+
1030+ return { success : true , info, tempFilePath } ;
1031+ } catch ( error ) {
1032+ return { success : false , error : String ( error ) } ;
1033+ }
1034+ } ) ;
1035+
1036+ ipcMain . handle ( IPC . SSH_DISCONNECT , async ( ) => {
1037+ try {
1038+ sshHandler . disconnect ( ) ;
1039+ return { success : true } ;
1040+ } catch ( error ) {
1041+ return { success : false , error : String ( error ) } ;
1042+ }
1043+ } ) ;
1044+
1045+ ipcMain . handle ( IPC . SSH_STATUS , async ( ) => {
1046+ return sshHandler . getStatus ( ) ;
1047+ } ) ;
1048+
1049+ ipcMain . handle ( IPC . SSH_SAVE_SESSION , async ( ) => {
1050+ const tempPath = sshHandler . getTempFilePath ( ) ;
1051+ if ( ! tempPath || ! fs . existsSync ( tempPath ) ) {
1052+ return { success : false , error : 'No SSH session data' } ;
1053+ }
1054+
1055+ const result = await dialog . showSaveDialog ( mainWindow ! , {
1056+ title : 'Save SSH Session' ,
1057+ defaultPath : path . basename ( tempPath ) ,
1058+ filters : [
1059+ { name : 'Log Files' , extensions : [ 'log' , 'txt' ] } ,
1060+ { name : 'All Files' , extensions : [ '*' ] } ,
1061+ ] ,
1062+ } ) ;
1063+
1064+ if ( result . canceled || ! result . filePath ) {
1065+ return { success : false , error : 'Cancelled' } ;
1066+ }
1067+
1068+ try {
1069+ fs . copyFileSync ( tempPath , result . filePath ) ;
1070+ return { success : true , filePath : result . filePath } ;
1071+ } catch ( error ) {
1072+ return { success : false , error : String ( error ) } ;
1073+ }
1074+ } ) ;
1075+
1076+ ipcMain . handle ( IPC . SSH_LIST_REMOTE_DIR , async ( _ , remotePath : string ) => {
1077+ try {
1078+ const files = await sshHandler . listRemoteDir ( remotePath ) ;
1079+ return { success : true , files } ;
1080+ } catch ( error ) {
1081+ return { success : false , error : String ( error ) } ;
1082+ }
1083+ } ) ;
1084+
1085+ ipcMain . handle ( IPC . SSH_DOWNLOAD_FILE , async ( _ , remotePath : string ) => {
1086+ try {
1087+ const localPath = await sshHandler . downloadRemoteFile ( remotePath ) ;
1088+ return { success : true , localPath } ;
1089+ } catch ( error ) {
1090+ return { success : false , error : String ( error ) } ;
1091+ }
1092+ } ) ;
1093+
9191094// === File Operations ===
9201095
9211096ipcMain . handle ( IPC . OPEN_FILE_DIALOG , async ( ) => {
0 commit comments