Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .github/workflows/jan-electron-build-nightly.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
name: Electron Builder - Nightly

on:
schedule:
- cron: '0 20 * * *' # Every day at 8 PM UTC
workflow_dispatch:

jobs:
test-and-verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'

- name: Install dependencies
run: |
corepack enable
yarn install

- name: Build Core
run: yarn build:core

- name: Build Extensions Web
run: yarn build:extensions-web

- name: Run Web App Tests
run: yarn workspace @janhq/web-app test

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install System Dependencies (Linux)
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libayatana-appindicator3-dev librsvg2-dev

- name: Run Rust Tests
run: |
cd src-tauri
cargo test

build-electron:
needs: test-and-verify
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install System Dependencies (Linux)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libayatana-appindicator3-dev librsvg2-dev

- name: Install dependencies
run: |
corepack enable
yarn install

- name: Download Sidecar Binaries
run: yarn download:bin

- name: Build Sidecar (Rust)
run: |
cd src-tauri
cargo build --release

- name: Build Web App
run: yarn build:web-app

- name: Build Electron App
run: yarn build:electron
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: electron-build-${{ matrix.os }}
path: electron-app/release/*

upload-release:
needs: build-electron
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts

- name: Create Nightly Release
uses: softprops/action-gh-release@v1
with:
tag_name: nightly-electron
name: Nightly Electron Build
prerelease: true
files: artifacts/**/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,8 @@ src-tauri/resources/
test-data
llm-docs
.claude/agents
electron-app/dist-web/
electron-app/dist/
dist-web/
**/dist/
**/dist-web/
54 changes: 54 additions & 0 deletions electron-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@jan/electron-app",
"version": "0.0.1",
"private": true,
"main": "dist/main.js",
"scripts": {
"build": "yarn copy:web && tsc",
"copy:web": "rimraf dist-web && cpx \"../web-app/dist-web/**/*\" \"dist-web/\"",
"dev": "electron .",
"pack": "electron-builder --dir",
"dist": "electron-builder"
},
"devDependencies": {
"cpx": "^1.5.0",
"electron": "^33.2.1",
"electron-builder": "^25.1.8",
"rimraf": "^3.0.2",
"typescript": "^5.7.2"
},
"dependencies": {
"electron-is-dev": "^3.0.1",
"get-port": "^7.1.0"
},
"build": {
"appId": "ai.jan.electron",
"productName": "Jan Electron",
"directories": {
"output": "release"
},
"files": [
"dist/**/*",
"dist-web/**/*",
"package.json"
],
"extraResources": [
{
"from": "../src-tauri/target/release/jan-app",
"to": "bin/jan-app"
}
],
"linux": {
"target": [
"AppImage",
"deb"
]
},
"win": {
"target": "nsis"
},
"mac": {
"target": "dmg"
}
}
}
153 changes: 153 additions & 0 deletions electron-app/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { app, BrowserWindow, ipcMain, dialog, protocol, net } from 'electron';
import * as path from 'path';
import { spawn, ChildProcess } from 'child_process';
import * as isDev from 'electron-is-dev';

let mainWindow: BrowserWindow | null = null;
let sidecarProcess: ChildProcess | null = null;
let serverPort: number | null = null;

// Register asset protocol to serve local files
protocol.registerSchemesAsPrivileged([
{ scheme: 'asset', privileges: { bypassCSP: true, corsEnabled: true, supportFetchAPI: true } }
]);

function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
webSecurity: true, // Keep enabled, we use custom protocol
},
});

const startUrl = isDev
? 'http://localhost:3001'
: `file://${path.join(__dirname, '../dist-web/index.html')}`;

console.log(`Loading URL: ${startUrl}`);
mainWindow.loadURL(startUrl);

if (isDev) {
mainWindow.webContents.openDevTools();
}

mainWindow.on('closed', () => {
mainWindow = null;
});
}

function startSidecar() {
// TODO: Adjust path to the actual binary
const binaryPath = isDev
? path.join(__dirname, '../../src-tauri/target/debug/jan-app') // Adjust extension for Windows
: path.join(process.resourcesPath, 'bin', 'jan-app');

console.log(`Starting sidecar: ${binaryPath}`);

sidecarProcess = spawn(binaryPath, ['--electron'], {
stdio: ['ignore', 'pipe', 'inherit'],
});

sidecarProcess.stdout?.on('data', (data) => {
const output = data.toString();
console.log(`[Sidecar]: ${output}`);

// Look for the port in the output
const match = output.match(/JAN_SERVER_PORT=(\d+)/);
if (match) {
serverPort = parseInt(match[1], 10);
console.log(`Sidecar server running on port: ${serverPort}`);
// Notify renderer
if (mainWindow) {
mainWindow.webContents.send('server-port', serverPort);
}
}
});

sidecarProcess.on('close', (code) => {
console.log(`Sidecar process exited with code ${code}`);
sidecarProcess = null;
});

sidecarProcess.on('error', (err) => {
console.error('Failed to start sidecar:', err);
});
}

app.on('ready', () => {
protocol.handle('asset', (req) => {
try {
const url = new URL(req.url);
let pathname = decodeURIComponent(url.pathname);

// Handle Windows paths (e.g., /C:/Users -> C:/Users)
if (process.platform === 'win32' && pathname.startsWith('/')) {
pathname = pathname.slice(1);
}

// Return response from file
return net.fetch('file://' + pathname);
} catch (error) {
console.error('Failed to handle asset request:', error);
return new Response('Not Found', { status: 404 });
}
});

createWindow();
startSidecar();
});

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});

app.on('activate', () => {
if (mainWindow === null) {
createWindow();
}
});

app.on('before-quit', () => {
if (sidecarProcess) {
sidecarProcess.kill();
}
});

// IPC handlers for UI commands
ipcMain.handle('get-server-port', () => {
return serverPort;
});

ipcMain.handle('open_dialog', async (event, args) => {
if (!mainWindow) return null;
const { title, defaultPath, filters, multiple, directory } = args || {};
const properties: any[] = [];
if (multiple) properties.push('multiSelections');
if (directory) properties.push('openDirectory');
else properties.push('openFile');

const result = await dialog.showOpenDialog(mainWindow, {
title,
defaultPath,
filters,
properties
});
return result.canceled ? null : (multiple ? result.filePaths : result.filePaths[0]);
});

ipcMain.handle('save_dialog', async (event, args) => {
if (!mainWindow) return null;
const { title, defaultPath, filters } = args || {};
const result = await dialog.showSaveDialog(mainWindow, {
title,
defaultPath,
filters
});
return result.canceled ? null : result.filePath;
});
8 changes: 8 additions & 0 deletions electron-app/src/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electronAPI', {
getServerPort: () => ipcRenderer.invoke('get-server-port'),
invoke: (channel: string, args: any) => ipcRenderer.invoke(channel, args),
onServerPort: (callback: (port: number) => void) =>
ipcRenderer.on('server-port', (_event, port) => callback(port)),
});
12 changes: 12 additions & 0 deletions electron-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Loading