@@ -4,13 +4,46 @@ const { app, BrowserWindow, ipcMain, shell, dialog } = require('electron');
44const path = require ( 'path' ) ;
55const fs = require ( 'fs' ) ;
66const crypto = require ( 'crypto' ) ;
7- const { spawn } = require ( 'child_process' ) ;
87
98// Project root resolved relative to this file — the GUI lives inside the repo at gui/.
109const ROOT = path . resolve ( __dirname , '..' ) ;
11- const PROFILES_DIR = path . join ( ROOT , 'data' , 'profiles' ) ;
12- const TSX = path . join ( ROOT , 'node_modules' , '.bin' , 'tsx' ) ;
13- const CLI_ENTRY = 'src/cli/index.ts' ;
10+ let PROFILES_DIR = path . join ( ROOT , 'data' , 'profiles' ) ;
11+
12+ // Packaged app: install dir is read-only (resources/app.asar). Everything writable
13+ // relocates into OS user-data; bundled chromium + stock profiles ride along as resources.
14+ function configurePackagedPaths ( ) {
15+ if ( ! app . isPackaged ) return ;
16+ const resources = process . resourcesPath ;
17+ const userData = app . getPath ( 'userData' ) ;
18+
19+ // bundled browsers (extraResources/browsers) — playwright honors this path
20+ process . env . PLAYWRIGHT_BROWSERS_PATH = path . join ( resources , 'browsers' ) ;
21+
22+ // stock profiles → userData copy (first run)
23+ const profilesDest = path . join ( userData , 'profiles' ) ;
24+ if ( ! fs . existsSync ( profilesDest ) ) {
25+ try { fs . cpSync ( path . join ( resources , 'extra-profiles' ) , profilesDest , { recursive : true } ) ; } catch { }
26+ }
27+ if ( fs . existsSync ( profilesDest ) ) process . env . GHOSTFRAME_PROFILES_DIR = profilesDest ;
28+
29+ // writable state → userData
30+ process . env . GHOSTFRAME_STATE_DIR = path . join ( userData , 'profiles-state' ) ;
31+ process . env . GHOSTFRAME_DATA_ROOT = ROOT ;
32+ process . env . GHOSTFRAME_PROJECT_ROOT = ROOT ;
33+ PROFILES_DIR = process . env . GHOSTFRAME_PROFILES_DIR || PROFILES_DIR ;
34+ }
35+ configurePackagedPaths ( ) ;
36+
37+ // In-process launcher access: load the esbuild-bundled CommonJS launcher.
38+ // Pre-built at packaging time (esbuild --bundle), so no TypeScript runtime is needed
39+ // and it works cleanly from inside an asar archive.
40+ let _launcher = null ;
41+ function loadLauncher ( ) {
42+ if ( ! _launcher ) {
43+ _launcher = require ( path . join ( ROOT , 'dist' , 'launcher.cjs' ) ) ;
44+ }
45+ return _launcher ;
46+ }
1447
1548// Canonical app identity — drives WM_CLASS + userData dir; must match the .desktop StartupWMClass.
1649app . setName ( 'GhostFrame' ) ;
@@ -98,10 +131,6 @@ function createWindow() {
98131}
99132
100133app . whenReady ( ) . then ( ( ) => {
101- // production sanity: tsx must exist for launch/fingerprint IPC
102- if ( ! fs . existsSync ( TSX ) ) {
103- dialog . showErrorBox ( 'GhostFrame' , 'Missing tsx runtime. Run `npm install` in ' + ROOT ) ;
104- }
105134 createWindow ( ) ;
106135 app . on ( 'activate' , ( ) => { if ( BrowserWindow . getAllWindows ( ) . length === 0 ) createWindow ( ) ; } ) ;
107136} ) ;
@@ -199,9 +228,15 @@ ipcMain.handle('profiles:create', async (event, data) => {
199228
200229ipcMain . handle ( 'profiles:launch' , async ( event , id ) => {
201230 if ( ! isValidId ( id ) ) throw new Error ( 'invalid profile id' ) ;
202- const child = spawn ( TSX , [ CLI_ENTRY , 'launch' , id ] , { cwd : ROOT , detached : true , stdio : 'ignore' , env : { ...process . env } } ) ;
203- child . unref ( ) ;
204- return { ok : true , pid : child . pid } ;
231+ const profileFile = path . join ( PROFILES_DIR , id + '.json' ) ;
232+ if ( ! fs . existsSync ( profileFile ) ) throw new Error ( 'profile not found' ) ;
233+ const profile = JSON . parse ( fs . readFileSync ( profileFile , 'utf8' ) ) ;
234+ const launcher = loadLauncher ( ) ;
235+ // Launch a *headed* browser — the user wants to interact with it from this machine.
236+ const { launchProfile } = launcher ;
237+ const result = await launchProfile ( profile , { headless : false , useGhostProxy : false } ) ;
238+ // Track open contexts for clean close; leave them running until the user closes them.
239+ return { ok : true , pid : process . pid } ;
205240} ) ;
206241
207242ipcMain . handle ( 'profiles:fingerprint' , async ( event , id ) => {
@@ -211,27 +246,24 @@ ipcMain.handle('profiles:fingerprint', async (event, id) => {
211246 const target = win && ! win . isDestroyed ( ) ? win : mainWindow ;
212247 if ( target && ! target . isDestroyed ( ) ) target . webContents . send ( 'fingerprint:progress' , { id, stage, message } ) ;
213248 } ;
249+ const profileFile = path . join ( PROFILES_DIR , id + '.json' ) ;
250+ if ( ! fs . existsSync ( profileFile ) ) throw new Error ( 'profile not found' ) ;
251+ const profile = JSON . parse ( fs . readFileSync ( profileFile , 'utf8' ) ) ;
214252 send ( 'start' , 'Launching headless browser to read fingerprint…' ) ;
215- return new Promise ( ( resolve , reject ) => {
216- const child = spawn ( TSX , [ CLI_ENTRY , 'fingerprint' , id ] , { cwd : ROOT , stdio : [ 'ignore' , 'pipe' , 'pipe' ] , env : { ...process . env } } ) ;
217- let stdout = '' ;
218- let stderr = '' ;
219- let settled = false ;
220- const killer = setTimeout ( ( ) => { if ( ! settled ) { settled = true ; try { child . kill ( 'SIGKILL' ) ; } catch ( e ) { } reject ( new Error ( 'fingerprint timed out after 90s' ) ) ; } } , 90000 ) ;
221- child . stdout . on ( 'data' , ( d ) => { const s = d . toString ( ) ; stdout += s ; for ( const line of s . split ( / \r ? \n / ) ) if ( line . trim ( ) ) send ( 'stdout' , line ) ; } ) ;
222- child . stderr . on ( 'data' , ( d ) => { const s = d . toString ( ) ; stderr += s ; for ( const line of s . split ( / \r ? \n / ) ) if ( line . trim ( ) ) send ( 'progress' , line ) ; } ) ;
223- child . on ( 'error' , ( err ) => { if ( ! settled ) { settled = true ; clearTimeout ( killer ) ; reject ( err ) ; } } ) ;
224- child . on ( 'close' , ( code ) => {
225- if ( settled ) return ;
226- settled = true ;
227- clearTimeout ( killer ) ;
228- if ( code !== 0 ) return reject ( new Error ( 'fingerprint exited with code ' + code + ( stderr ? '\n' + stderr . slice ( - 1500 ) : '' ) ) ) ;
229- const parsed = parseReadback ( stdout ) ;
230- if ( ! parsed ) return reject ( new Error ( 'failed to parse FingerprintReadback JSON' + ( stderr ? '\n' + stderr . slice ( - 1500 ) : '' ) ) ) ;
231- send ( 'done' , 'Fingerprint read.' ) ;
232- resolve ( parsed ) ;
233- } ) ;
234- } ) ;
253+ const launcher = loadLauncher ( ) ;
254+ const { launchProfile, readFingerprint, close } = launcher ;
255+ const t0 = Date . now ( ) ;
256+ let session = null ;
257+ try {
258+ session = await launchProfile ( profile , { headless : true , useGhostProxy : false } ) ;
259+ const rb = await readFingerprint ( session . context , profile ) ;
260+ send ( 'done' , 'Fingerprint read.' ) ;
261+ return rb ;
262+ } catch ( e ) {
263+ throw new Error ( String ( e && e . message ? e . message : e ) ) ;
264+ } finally {
265+ if ( session ) await close ( session . context ) . catch ( ( ) => { } ) ;
266+ }
235267} ) ;
236268
237269process . on ( 'uncaughtException' , ( err ) => {
0 commit comments