@@ -4,6 +4,7 @@ import { app, ipcMain, dialog } from "electron";
44import { promises as fs } from "fs" ;
55import { exec } from "child_process" ;
66import { promisify } from "util" ;
7+ import unzipper from "unzipper" ;
78
89const execAsync = promisify ( exec ) ;
910const __filename = fileURLToPath ( import . meta. url ) ;
@@ -363,9 +364,124 @@ class P2PAppRegistry {
363364 await this . saveRegistry ( ) ;
364365 }
365366
367+ parseGitModules ( content ) {
368+ const submodules = [ ] ;
369+ let current = null ;
370+ for ( const line of content . split ( '\n' ) ) {
371+ const trimmed = line . trim ( ) ;
372+ if ( trimmed . startsWith ( '[submodule' ) ) {
373+ current = { } ;
374+ submodules . push ( current ) ;
375+ } else if ( current && trimmed . startsWith ( 'path =' ) ) {
376+ current . subPath = trimmed . slice ( 'path =' . length ) . trim ( ) ;
377+ } else if ( current && trimmed . startsWith ( 'url =' ) ) {
378+ current . url = trimmed . slice ( 'url =' . length ) . trim ( ) ;
379+ }
380+ }
381+ return submodules
382+ . filter ( s => s . subPath && s . url && s . url . includes ( 'github.com' ) )
383+ . map ( s => ( {
384+ repo : s . url . replace ( / ^ h t t p s ? : \/ \/ g i t h u b \. c o m \/ / , '' ) . replace ( / \. g i t $ / , '' ) ,
385+ dir : path . basename ( s . subPath )
386+ } ) ) ;
387+ }
388+
389+ async updateSubmodulesFromGitHub ( ) {
390+ let SUBMODULE_APPS ;
391+ try {
392+ const gitmodulesPath = path . join ( app . getAppPath ( ) , '.gitmodules' ) ;
393+ const content = await fs . readFile ( gitmodulesPath , 'utf8' ) ;
394+ SUBMODULE_APPS = this . parseGitModules ( content ) ;
395+ if ( ! SUBMODULE_APPS . length ) throw new Error ( 'No GitHub submodules found in .gitmodules' ) ;
396+ } catch ( err ) {
397+ console . error ( 'Could not parse .gitmodules:' , err . message ) ;
398+ return { success : false , error : `Could not read .gitmodules: ${ err . message } ` } ;
399+ }
400+
401+ // In packaged builds, __dirname points inside app.asar (read-only)
402+ // We need to write to app.asar.unpacked instead
403+ let p2pDir ;
404+ if ( app . isPackaged ) {
405+ const appPath = app . getAppPath ( ) ;
406+ const unpackedPath = appPath . replace ( / \. a s a r $ / , '.asar.unpacked' ) ;
407+ p2pDir = path . join ( unpackedPath , 'src' , 'pages' , 'p2p' ) ;
408+ } else {
409+ p2pDir = path . join ( __dirname , 'pages' , 'p2p' ) ;
410+ }
411+ const errors = [ ] ;
412+
413+ for ( const mod of SUBMODULE_APPS ) {
414+ try {
415+ const zipUrl = `https://github.com/${ mod . repo } /archive/HEAD.zip` ;
416+ const controller = new AbortController ( ) ;
417+ const timer = setTimeout ( ( ) => controller . abort ( ) , 60000 ) ;
418+
419+ let response ;
420+ try {
421+ response = await fetch ( zipUrl , { signal : controller . signal } ) ;
422+ } finally {
423+ clearTimeout ( timer ) ;
424+ }
425+
426+ if ( ! response . ok ) {
427+ throw new Error ( `HTTP ${ response . status } ` ) ;
428+ }
429+
430+ const buffer = Buffer . from ( await response . arrayBuffer ( ) ) ;
431+ const directory = await unzipper . Open . buffer ( buffer ) ;
432+ const targetDir = path . join ( p2pDir , mod . dir ) ;
433+
434+ // Clear existing contents before extracting fresh copy
435+ try {
436+ const existing = await fs . readdir ( targetDir ) ;
437+ await Promise . all (
438+ existing . map ( e => fs . rm ( path . join ( targetDir , e ) , { recursive : true , force : true } ) )
439+ ) ;
440+ } catch {
441+ await fs . mkdir ( targetDir , { recursive : true } ) ;
442+ }
443+
444+ // Extract all files, stripping the top-level GitHub zip folder (e.g. "peerchat-main/")
445+ for ( const file of directory . files ) {
446+ const relativePath = file . path . replace ( / ^ [ ^ / ] + \/ / , '' ) ;
447+ if ( ! relativePath ) continue ;
448+
449+ const destPath = path . join ( targetDir , relativePath ) ;
450+ if ( file . type === 'Directory' || relativePath . endsWith ( '/' ) ) {
451+ await fs . mkdir ( destPath , { recursive : true } ) ;
452+ } else {
453+ await fs . mkdir ( path . dirname ( destPath ) , { recursive : true } ) ;
454+ const content = await file . buffer ( ) ;
455+ await fs . writeFile ( destPath , content ) ;
456+ }
457+ }
458+ } catch ( error ) {
459+ console . error ( `Failed to update ${ mod . dir } :` , error ) ;
460+ errors . push ( `${ mod . dir } : ${ error . message } ` ) ;
461+ }
462+ }
463+
464+ if ( errors . length > 0 ) {
465+ return {
466+ success : false ,
467+ error : `Failed to update some apps: ${ errors . join ( '; ' ) } `
468+ } ;
469+ }
470+
471+ return {
472+ success : true ,
473+ message : 'P2P apps updated to latest versions. Refresh to see changes.'
474+ } ;
475+ }
476+
366477 async updateSubmodules ( ) {
478+ // Packaged builds have no .git and no git binary — use GitHub zip downloads instead
479+ if ( app . isPackaged ) {
480+ return await this . updateSubmodulesFromGitHub ( ) ;
481+ }
482+
483+ // Development: use git submodule update
367484 try {
368- // Get the project root directory (where .git is located)
369485 const projectRoot = app . getAppPath ( ) ;
370486
371487 // Update all submodules to their latest commits
0 commit comments