Skip to content

Commit 6b80751

Browse files
committed
dist: packaged desktop installers — Linux AppImage (bundled browser), GH Actions releases for win/mac/linux
1 parent 1286a94 commit 6b80751

10 files changed

Lines changed: 5674 additions & 41 deletions

File tree

.github/workflows/release.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: release
2+
3+
on:
4+
push:
5+
tags: ['v*']
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
bundle:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: actions/setup-node@v4
17+
with: { node-version: '20', cache: npm }
18+
- run: npm ci
19+
- run: npm ci --prefix packaging
20+
21+
build-windows:
22+
needs: bundle
23+
runs-on: windows-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
- uses: actions/setup-node@v4
27+
with: { node-version: '20', cache: npm }
28+
- run: npm ci
29+
- run: npm run build
30+
- run: npx playwright install chromium
31+
- run: node packaging/stage.mjs
32+
- run: npx electron-builder --win nsis portable --projectDir packaging
33+
- name: Upload artifacts
34+
uses: softprops/action-gh-release@v2
35+
with:
36+
files: |
37+
packaging/dist/appimage/GhostFrame-Setup-*.exe
38+
packaging/dist/appimage/GhostFrame-Setup-*.msi
39+
packaging/dist/appimage/GhostFrame-Setup-*.zip
40+
41+
build-macos:
42+
needs: bundle
43+
runs-on: macos-latest
44+
steps:
45+
- uses: actions/checkout@v4
46+
- uses: actions/setup-node@v4
47+
with: { node-version: '20', cache: npm }
48+
- run: npm ci
49+
- run: npm run build
50+
- run: npx playwright install chromium
51+
- run: node packaging/stage.mjs
52+
- run: npx electron-builder --mac dmg zip --projectDir packaging
53+
- name: Upload artifacts
54+
uses: softprops/action-gh-release@v2
55+
with:
56+
files: |
57+
packaging/dist/appimage/GhostFrame-*.dmg
58+
packaging/dist/appimage/GhostFrame-*.zip
59+
60+
build-linux:
61+
needs: bundle
62+
runs-on: ubuntu-latest
63+
steps:
64+
- uses: actions/checkout@v4
65+
- uses: actions/setup-node@v4
66+
with: { node-version: '20', cache: npm }
67+
- run: npm ci
68+
- run: npm run build
69+
- run: npx playwright install chromium
70+
- run: node packaging/stage.mjs
71+
- run: npx electron-builder --linux AppImage deb --projectDir packaging
72+
- name: Upload artifacts
73+
uses: softprops/action-gh-release@v2
74+
with:
75+
files: |
76+
packaging/dist/appimage/GhostFrame-*.AppImage
77+
packaging/dist/appimage/GhostFrame-*.deb

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,9 @@ mobile/ghostframe.apk
3838
.vscode/
3939
*.swp
4040
*.swo
41+
42+
# --- packaging (electron-builder stage + outputs + browsers) ------------------
43+
packaging/app/
44+
packaging/dist/
45+
packaging/node_modules/
46+
packaging/browsers/

browsers.json

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{
2+
"comment": "Do not edit this file, use utils/roll_browser.js",
3+
"browsers": [
4+
{
5+
"name": "chromium",
6+
"revision": "1234",
7+
"installByDefault": true,
8+
"browserVersion": "151.0.7922.34",
9+
"title": "Chrome for Testing"
10+
},
11+
{
12+
"name": "chromium-headless-shell",
13+
"revision": "1234",
14+
"installByDefault": true,
15+
"browserVersion": "151.0.7922.34",
16+
"title": "Chrome Headless Shell"
17+
},
18+
{
19+
"name": "chromium-tip-of-tree",
20+
"revision": "1433",
21+
"installByDefault": false,
22+
"browserVersion": "151.0.7893.0",
23+
"title": "Chrome Canary for Testing"
24+
},
25+
{
26+
"name": "chromium-tip-of-tree-headless-shell",
27+
"revision": "1433",
28+
"installByDefault": false,
29+
"browserVersion": "151.0.7893.0",
30+
"title": "Chrome Canary Headless Shell"
31+
},
32+
{
33+
"name": "firefox",
34+
"revision": "1538",
35+
"installByDefault": true,
36+
"browserVersion": "153.0",
37+
"title": "Firefox"
38+
},
39+
{
40+
"name": "firefox-beta",
41+
"revision": "1526",
42+
"installByDefault": false,
43+
"browserVersion": "152.0b1",
44+
"title": "Firefox Beta"
45+
},
46+
{
47+
"name": "webkit",
48+
"revision": "2336",
49+
"installByDefault": true,
50+
"revisionOverrides": {
51+
"mac14": "2251",
52+
"mac14-arm64": "2251",
53+
"ubuntu20.04-x64": "2092",
54+
"ubuntu20.04-arm64": "2092"
55+
},
56+
"browserVersion": "26.5",
57+
"title": "WebKit"
58+
},
59+
{
60+
"name": "ffmpeg",
61+
"revision": "1011",
62+
"installByDefault": true
63+
},
64+
{
65+
"name": "winldd",
66+
"revision": "1007",
67+
"installByDefault": false
68+
},
69+
{
70+
"name": "android",
71+
"revision": "1001",
72+
"installByDefault": false
73+
}
74+
]
75+
}

gui/main.js

Lines changed: 63 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,46 @@ const { app, BrowserWindow, ipcMain, shell, dialog } = require('electron');
44
const path = require('path');
55
const fs = require('fs');
66
const 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/.
109
const 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.
1649
app.setName('GhostFrame');
@@ -98,10 +131,6 @@ function createWindow() {
98131
}
99132

100133
app.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

200229
ipcMain.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

207242
ipcMain.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

237269
process.on('uncaughtException', (err) => {

packaging/electron-builder.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
appId: com.ghostframe.app
2+
productName: GhostFrame
3+
asar: true
4+
asarUnpack:
5+
- dist/launcher.cjs
6+
- dist/inject.js
7+
directories:
8+
output: dist/appimage
9+
buildResources: resources
10+
files:
11+
- package.json
12+
- gui/**
13+
- dist/launcher.cjs
14+
- dist/inject.js
15+
- browsers.json
16+
extraResources:
17+
- from: ../data/profiles
18+
to: extra-profiles
19+
- from: app/browsers
20+
to: browsers
21+
linux:
22+
target: AppImage
23+
category: Network
24+
icon: resources/icon.png
25+
artifactName: GhostFrame-${arch}.${ext}
26+
win:
27+
target:
28+
- nsis
29+
- portable
30+
icon: resources/icon.png
31+
artifactName: GhostFrame-Setup-${arch}.${ext}
32+
nsis:
33+
oneClick: false
34+
allowToChangeInstallationDirectory: true
35+
createDesktopShortcut: true
36+
shortcutName: GhostFrame
37+
mac:
38+
target:
39+
- dmg
40+
- zip
41+
icon: resources/icon.png
42+
artifactName: GhostFrame-${arch}.${ext}
43+
category: public.app-category.utilities
44+
hardenedRuntime: true
45+
gatekeeperAssess: false
46+
identity: null

0 commit comments

Comments
 (0)