diff --git a/.github/workflows/desktop-build.yml b/.github/workflows/desktop-build.yml
new file mode 100644
index 000000000..741edacc8
--- /dev/null
+++ b/.github/workflows/desktop-build.yml
@@ -0,0 +1,117 @@
+name: Desktop App Build
+
+on:
+ push:
+ tags:
+ - 'desktop-v*'
+ workflow_dispatch:
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ include:
+ - os: macos-latest
+ platform: mac
+ arch: universal
+ - os: windows-latest
+ platform: win
+ arch: x64
+ - os: ubuntu-latest
+ platform: linux
+ arch: x64
+
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 10.5.2
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Build packages
+ run: pnpm turbo run build --filter=@lightfast/desktop^...
+
+ - name: Build Electron app for macOS
+ if: matrix.platform == 'mac'
+ run: pnpm dist:desktop:mac
+ env:
+ CSC_LINK: ${{ secrets.MAC_CERTS }}
+ CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTS_PASSWORD }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+
+ - name: Build Electron app for Windows
+ if: matrix.platform == 'win'
+ run: pnpm dist:desktop:win
+ env:
+ CSC_LINK: ${{ secrets.WIN_CERTS }}
+ CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTS_PASSWORD }}
+
+ - name: Build Electron app for Linux
+ if: matrix.platform == 'linux'
+ run: pnpm dist:desktop:linux
+
+ - name: Upload artifacts - macOS
+ if: matrix.platform == 'mac'
+ uses: actions/upload-artifact@v4
+ with:
+ name: lightfast-desktop-mac
+ path: |
+ apps/desktop/out/*.dmg
+ apps/desktop/out/*.zip
+
+ - name: Upload artifacts - Windows
+ if: matrix.platform == 'win'
+ uses: actions/upload-artifact@v4
+ with:
+ name: lightfast-desktop-win
+ path: apps/desktop/out/*.exe
+
+ - name: Upload artifacts - Linux
+ if: matrix.platform == 'linux'
+ uses: actions/upload-artifact@v4
+ with:
+ name: lightfast-desktop-linux
+ path: |
+ apps/desktop/out/*.AppImage
+ apps/desktop/out/*.deb
+
+ release:
+ needs: build
+ runs-on: ubuntu-latest
+ if: startsWith(github.ref, 'refs/tags/')
+
+ steps:
+ - name: Download all artifacts
+ uses: actions/download-artifact@v4
+
+ - name: Create Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: |
+ lightfast-desktop-mac/*.dmg
+ lightfast-desktop-mac/*.zip
+ lightfast-desktop-win/*.exe
+ lightfast-desktop-linux/*.AppImage
+ lightfast-desktop-linux/*.deb
+ draft: false
+ prerelease: false
+ generate_release_notes: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/apps/desktop/.gitignore b/apps/desktop/.gitignore
new file mode 100644
index 000000000..5d648a073
--- /dev/null
+++ b/apps/desktop/.gitignore
@@ -0,0 +1,34 @@
+# Dependencies
+node_modules/
+
+# Build outputs
+dist/
+dist-electron/
+out/
+
+# Electron
+electron-builder.yml
+*.unpacked
+
+# Environment
+.env
+.env.local
+
+# IDE
+.vscode/
+.idea/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Testing
+coverage/
+.nyc_output/
\ No newline at end of file
diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts
new file mode 100644
index 000000000..327dd6040
--- /dev/null
+++ b/apps/desktop/electron/main.ts
@@ -0,0 +1,69 @@
+import { app, BrowserWindow, shell, ipcMain } from 'electron';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+let mainWindow: BrowserWindow | null = null;
+
+function createWindow(): void {
+ mainWindow = new BrowserWindow({
+ width: 1200,
+ height: 800,
+ minWidth: 800,
+ minHeight: 600,
+ title: 'Lightfast',
+ webPreferences: {
+ nodeIntegration: false,
+ contextIsolation: true,
+ preload: path.join(__dirname, 'preload.js'),
+ },
+ show: false,
+ backgroundColor: '#000000',
+ titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
+ });
+
+ mainWindow.once('ready-to-show', () => {
+ mainWindow?.show();
+ });
+
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
+ shell.openExternal(url);
+ return { action: 'deny' };
+ });
+
+ if (process.env.NODE_ENV === 'development') {
+ mainWindow.loadURL('http://localhost:5173');
+ } else {
+ mainWindow.loadFile(path.join(__dirname, '../index.html'));
+ }
+
+ mainWindow.on('closed', () => {
+ mainWindow = null;
+ });
+}
+
+app.whenReady().then(() => {
+ createWindow();
+
+ app.on('activate', () => {
+ if (BrowserWindow.getAllWindows().length === 0) {
+ createWindow();
+ }
+ });
+});
+
+app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ app.quit();
+ }
+});
+
+ipcMain.handle('app:getVersion', (): string => {
+ return app.getVersion();
+});
+
+ipcMain.handle('app:getName', (): string => {
+ return app.getName();
+});
\ No newline at end of file
diff --git a/apps/desktop/electron/preload.ts b/apps/desktop/electron/preload.ts
new file mode 100644
index 000000000..ff9c11f93
--- /dev/null
+++ b/apps/desktop/electron/preload.ts
@@ -0,0 +1,9 @@
+import { contextBridge, ipcRenderer } from 'electron';
+
+contextBridge.exposeInMainWorld('electron', {
+ app: {
+ getVersion: () => ipcRenderer.invoke('app:getVersion'),
+ getName: () => ipcRenderer.invoke('app:getName'),
+ },
+ platform: process.platform,
+});
\ No newline at end of file
diff --git a/apps/desktop/eslint.config.js b/apps/desktop/eslint.config.js
new file mode 100644
index 000000000..6a1a86754
--- /dev/null
+++ b/apps/desktop/eslint.config.js
@@ -0,0 +1,8 @@
+import baseConfig from "@repo/eslint-config/base.js";
+
+export default [
+ ...baseConfig,
+ {
+ ignores: ["dist/**", "out/**", "*.config.ts", "*.config.js"],
+ },
+];
\ No newline at end of file
diff --git a/apps/desktop/index.html b/apps/desktop/index.html
new file mode 100644
index 000000000..779c4cb5b
--- /dev/null
+++ b/apps/desktop/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Lightfast Desktop
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
new file mode 100644
index 000000000..7ff21d1a5
--- /dev/null
+++ b/apps/desktop/package.json
@@ -0,0 +1,94 @@
+{
+ "name": "@lightfast/desktop",
+ "version": "0.1.0",
+ "private": true,
+ "description": "Lightfast Desktop Application",
+ "main": "dist-electron/main.js",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "dist": "npm run build && electron-builder",
+ "dist:mac": "npm run build && electron-builder --mac",
+ "dist:win": "npm run build && electron-builder --win",
+ "dist:linux": "npm run build && electron-builder --linux",
+ "lint": "eslint . --max-warnings 0",
+ "typecheck": "tsc --noEmit",
+ "clean": "rm -rf dist out"
+ },
+ "dependencies": {
+ "electron-log": "^5.4.2",
+ "electron-updater": "^6.6.2"
+ },
+ "devDependencies": {
+ "@repo/eslint-config": "workspace:*",
+ "@repo/prettier-config": "workspace:*",
+ "@repo/typescript-config": "workspace:*",
+ "@types/node": "catalog:",
+ "@types/react": "catalog:react19",
+ "@types/react-dom": "catalog:react19",
+ "@vitejs/plugin-react": "^5.0.0",
+ "electron": "^37.2.6",
+ "electron-builder": "^26.0.12",
+ "eslint": "catalog:",
+ "npm-run-all": "^4.1.5",
+ "prettier": "catalog:",
+ "react": "catalog:react19",
+ "react-dom": "catalog:react19",
+ "typescript": "catalog:",
+ "vite": "^7.1.2",
+ "vite-plugin-electron": "^0.29.0",
+ "vite-plugin-electron-renderer": "^0.14.6",
+ "wait-on": "^8.0.4"
+ },
+ "build": {
+ "appId": "com.lightfast.desktop",
+ "productName": "Lightfast",
+ "directories": {
+ "output": "out"
+ },
+ "files": [
+ "dist",
+ "dist-electron",
+ "!node_modules/**/*"
+ ],
+ "mac": {
+ "category": "public.app-category.developer-tools",
+ "target": [
+ {
+ "target": "dmg",
+ "arch": ["x64", "arm64"]
+ },
+ {
+ "target": "zip",
+ "arch": ["x64", "arm64"]
+ }
+ ]
+ },
+ "win": {
+ "target": [
+ {
+ "target": "nsis",
+ "arch": ["x64"]
+ }
+ ]
+ },
+ "linux": {
+ "target": [
+ {
+ "target": "AppImage",
+ "arch": ["x64"]
+ },
+ {
+ "target": "deb",
+ "arch": ["x64"]
+ }
+ ],
+ "category": "Development"
+ },
+ "nsis": {
+ "oneClick": false,
+ "allowToChangeInstallationDirectory": true
+ }
+ },
+ "prettier": "@repo/prettier-config"
+}
\ No newline at end of file
diff --git a/apps/desktop/public/icon.png b/apps/desktop/public/icon.png
new file mode 100644
index 000000000..92e9d4795
--- /dev/null
+++ b/apps/desktop/public/icon.png
@@ -0,0 +1,2 @@
+# Placeholder for icon.png
+# Replace this with your actual icon file (512x512 PNG recommended)
\ No newline at end of file
diff --git a/apps/desktop/src/renderer/App.tsx b/apps/desktop/src/renderer/App.tsx
new file mode 100644
index 000000000..f46786828
--- /dev/null
+++ b/apps/desktop/src/renderer/App.tsx
@@ -0,0 +1,94 @@
+import React, { useEffect, useState } from 'react';
+
+interface AppInfo {
+ name: string;
+ version: string;
+ platform: string;
+}
+
+declare global {
+ interface Window {
+ electron: {
+ app: {
+ getName: () => Promise;
+ getVersion: () => Promise;
+ };
+ platform: string;
+ };
+ }
+}
+
+function App(): React.ReactElement {
+ const [appInfo, setAppInfo] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const loadAppInfo = async (): Promise => {
+ try {
+ const [name, version] = await Promise.all([
+ window.electron.app.getName(),
+ window.electron.app.getVersion(),
+ ]);
+
+ setAppInfo({
+ name,
+ version,
+ platform: window.electron.platform,
+ });
+ } catch (error) {
+ console.error('Failed to load app info:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadAppInfo();
+ }, []);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/apps/desktop/src/renderer/index.css b/apps/desktop/src/renderer/index.css
new file mode 100644
index 000000000..d156a25e8
--- /dev/null
+++ b/apps/desktop/src/renderer/index.css
@@ -0,0 +1,155 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ height: 100vh;
+ overflow: hidden;
+}
+
+.app {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ padding: 20px;
+}
+
+.loading {
+ font-size: 24px;
+ animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+.app-header {
+ text-align: center;
+ max-width: 600px;
+ width: 100%;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ border-radius: 20px;
+ padding: 40px;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
+}
+
+.logo-container h1 {
+ font-size: 36px;
+ margin-bottom: 30px;
+ background: linear-gradient(to right, #ffd700, #ffed4e);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.info-container h2 {
+ font-size: 28px;
+ margin-bottom: 10px;
+}
+
+.info-container p {
+ font-size: 16px;
+ margin-bottom: 20px;
+ opacity: 0.9;
+}
+
+.app-info {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ padding: 20px;
+ margin: 20px 0;
+}
+
+.app-info p {
+ margin: 5px 0;
+ font-size: 14px;
+}
+
+.actions {
+ margin: 30px 0;
+}
+
+.update-button {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ border-radius: 8px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.update-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
+}
+
+.update-button:active {
+ transform: translateY(0);
+}
+
+.update-status {
+ margin-top: 15px;
+ font-size: 14px;
+}
+
+.status-checking {
+ color: #ffd700;
+}
+
+.status-available {
+ color: #4ade80;
+}
+
+.status-downloaded {
+ color: #60a5fa;
+}
+
+.status-error {
+ color: #f87171;
+}
+
+.tech-stack {
+ margin-top: 30px;
+ text-align: left;
+}
+
+.tech-stack h3 {
+ font-size: 18px;
+ margin-bottom: 10px;
+ text-align: center;
+}
+
+.tech-stack ul {
+ list-style: none;
+ padding: 0;
+}
+
+.tech-stack li {
+ padding: 8px 15px;
+ margin: 5px 0;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 5px;
+ transition: background 0.2s;
+}
+
+.tech-stack li:hover {
+ background: rgba(255, 255, 255, 0.15);
+}
\ No newline at end of file
diff --git a/apps/desktop/src/renderer/main.tsx b/apps/desktop/src/renderer/main.tsx
new file mode 100644
index 000000000..fb7690c50
--- /dev/null
+++ b/apps/desktop/src/renderer/main.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './index.css';
+
+const root = document.getElementById('root');
+
+if (!root) {
+ throw new Error('Root element not found');
+}
+
+ReactDOM.createRoot(root).render(
+
+
+
+);
\ No newline at end of file
diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json
new file mode 100644
index 000000000..99a8ac497
--- /dev/null
+++ b/apps/desktop/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "@repo/typescript-config/nextjs.json",
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "jsx": "react-jsx",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "noEmit": true,
+ "types": ["vite/client", "node"],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src/**/*", "electron/**/*", "vite.config.mts"],
+ "exclude": ["node_modules", "dist", "dist-electron", "out"]
+}
\ No newline at end of file
diff --git a/apps/desktop/vite.config.mts b/apps/desktop/vite.config.mts
new file mode 100644
index 000000000..7111dbd8f
--- /dev/null
+++ b/apps/desktop/vite.config.mts
@@ -0,0 +1,26 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import electron from 'vite-plugin-electron/simple';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = fileURLToPath(new URL('.', import.meta.url));
+
+export default defineConfig({
+ plugins: [
+ react(),
+ electron({
+ main: {
+ entry: 'electron/main.ts',
+ },
+ preload: {
+ input: __dirname + 'electron/preload.ts',
+ },
+ renderer: {},
+ }),
+ ],
+ resolve: {
+ alias: {
+ '@': __dirname + 'src',
+ },
+ },
+});
\ No newline at end of file
diff --git a/apps/www/src/app/(app)/download/page.tsx b/apps/www/src/app/(app)/download/page.tsx
new file mode 100644
index 000000000..86e39cd94
--- /dev/null
+++ b/apps/www/src/app/(app)/download/page.tsx
@@ -0,0 +1,161 @@
+import type { Metadata } from "next";
+import { DownloadIcon, MonitorIcon, AppleIcon, WindowsIcon, LinuxIcon } from "lucide-react";
+
+export const metadata: Metadata = {
+ title: "Download Lightfast Desktop",
+ description: "Download Lightfast Desktop app for macOS, Windows, and Linux",
+};
+
+interface Platform {
+ name: string;
+ icon: React.ComponentType<{ className?: string }>;
+ primary: string;
+ secondary?: string;
+ downloadUrl: string;
+ version: string;
+ requirements: string[];
+}
+
+const platforms: Platform[] = [
+ {
+ name: "macOS",
+ icon: AppleIcon,
+ primary: "Download for macOS",
+ secondary: "Universal (Intel + Apple Silicon)",
+ downloadUrl: "https://github.com/lightfastai/lightfast/releases/latest/download/Lightfast.dmg",
+ version: "0.1.0",
+ requirements: ["macOS 10.15 or later", "64-bit processor"],
+ },
+ {
+ name: "Windows",
+ icon: WindowsIcon,
+ primary: "Download for Windows",
+ secondary: "64-bit",
+ downloadUrl: "https://github.com/lightfastai/lightfast/releases/latest/download/Lightfast-Setup.exe",
+ version: "0.1.0",
+ requirements: ["Windows 10 or later", "64-bit processor"],
+ },
+ {
+ name: "Linux",
+ icon: LinuxIcon,
+ primary: "Download for Linux",
+ secondary: "AppImage",
+ downloadUrl: "https://github.com/lightfastai/lightfast/releases/latest/download/Lightfast.AppImage",
+ version: "0.1.0",
+ requirements: ["64-bit Linux distribution", "GLIBC 2.29 or later"],
+ },
+];
+
+export default function DownloadPage() {
+ return (
+
+
+
+
+
+
Download Lightfast Desktop
+
+ Experience the power of Lightfast with our native desktop application.
+ Available for macOS, Windows, and Linux.
+
+
+
+
+ {platforms.map((platform) => {
+ const Icon = platform.icon;
+ return (
+
+
+
+
+
{platform.name}
+
Version {platform.version}
+
+
+
+
+ {platform.primary}
+
+
+ {platform.secondary && (
+
{platform.secondary}
+ )}
+
+
+
Requirements:
+
+ {platform.requirements.map((req) => (
+ - {req}
+ ))}
+
+
+
+
+ );
+ })}
+
+
+
+
Features
+
+
+
+
+
Native Performance
+
+ Built with Electron for optimal performance on your desktop
+
+
+
+
+
+
+
Type-Safe Architecture
+
+ Fully typed with TypeScript for reliability and maintainability
+
+
+
+
+
+
+
Auto Updates
+
+ Automatic updates ensure you always have the latest features
+
+
+
+
+
+
+
Cross-Platform
+
+ Consistent experience across macOS, Windows, and Linux
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/www/src/components/site-header.tsx b/apps/www/src/components/site-header.tsx
index c2396e158..a141d0799 100644
--- a/apps/www/src/components/site-header.tsx
+++ b/apps/www/src/components/site-header.tsx
@@ -19,6 +19,12 @@ export function SiteHeader() {
>
Docs
+
+ Download
+