#!/bin/bash set -euo pipefail usage() { echo "Usage: $0 [project-name]" echo "If no project name is given, you will be prompted." } valid_name() { [[ "$1" =~ ^[A-Za-z0-9_-]+$ ]] } if [[ "${1-}" == "-h" || "${1-}" == "--help" ]]; then usage; exit 0 fi PROJ="${1-}" if [[ -n "$PROJ" ]] && ! valid_name "$PROJ"; then echo "Invalid project name: $PROJ (use letters, numbers, hyphen, underscore)" PROJ="" fi while [[ -z "$PROJ" ]]; do read -r -p "Enter project name (letters, numbers, -, _ only): " PROJ if [[ -z "$PROJ" ]]; then continue fi if ! valid_name "$PROJ"; then echo "Invalid project name. Use letters, numbers, hyphen, underscore." PROJ="" fi done echo ">> Creating Electron+React+FastAPI boilerplate in '$PROJ' ..." # --- Directory Structure --- mkdir -p "$PROJ/backend" mkdir -p "$PROJ/frontend" # --- Backend: Python FastAPI+SQLite --- cat > "$PROJ/backend/requirements.txt" < "$PROJ/backend/db.py" < "$PROJ/backend/main.py" < electron.cjs <<'EOF' const { app, BrowserWindow } = require('electron'); const path = require('path'); const fs = require('fs'); const { spawn } = require('child_process'); const distIndex = path.join(__dirname, 'dist', 'index.html'); const useDevServer = process.env.ELECTRON_DEV === 'true' || (!app.isPackaged && process.env.ELECTRON_DEV !== 'false' && !fs.existsSync(distIndex)); const skipBackend = ['1', 'true', 'yes'].includes(String(process.env.SKIP_ELECTRON_BACKEND || '').toLowerCase()); const skipFrontend = ['1', 'true', 'yes'].includes(String(process.env.SKIP_ELECTRON_FRONTEND || '').toLowerCase()); const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173'; const appTitle = process.env.APP_TITLE || process.env.npm_package_name || "React, FastAPI, SQLite, WebSocket"; let backendProc = null; let frontendProc = null; function waitFor(url, timeoutMs = 20000, intervalMs = 300) { const http = require('http'); const start = Date.now(); return new Promise((resolve, reject) => { const check = () => { http .get(url, res => { res.destroy(); resolve(); }) .on('error', () => { if (Date.now() - start > timeoutMs) { reject(new Error(`Timed out waiting for ${url}`)); } else { setTimeout(check, intervalMs); } }); }; check(); }); } function cleanup() { if (backendProc) backendProc.kill(); if (frontendProc) frontendProc.kill(); } async function createWindow() { const win = new BrowserWindow({ width: 1200, height: 800, title: appTitle, webPreferences: { nodeIntegration: false, contextIsolation: true, } }); // Prevent the page title from overwriting our window title win.on('page-title-updated', (event) => { event.preventDefault(); if (win.getTitle() !== appTitle) { win.setTitle(appTitle); } }); if (useDevServer) { if (!skipFrontend) { frontendProc = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['run', 'dev'], { cwd: __dirname, shell: false, stdio: 'ignore' }); } try { await waitFor(frontendUrl); win.loadURL(frontendUrl); } catch (err) { console.error(err); cleanup(); app.quit(); } } else if (fs.existsSync(distIndex)) { win.loadFile(distIndex); } else { console.error('dist/index.html not found. Run "npm run build" or set ELECTRON_DEV=true to use the dev server.'); cleanup(); app.quit(); } } function backendPython() { const venvBin = process.platform === 'win32' ? '.venv/Scripts/python.exe' : '.venv/bin/python'; const candidate = path.join(__dirname, '../backend', venvBin); if (fs.existsSync(candidate)) return candidate; return process.platform === 'win32' ? 'python' : 'python3'; } app.whenReady().then(() => { if (!skipBackend) { backendProc = spawn(backendPython(), ['-m', 'uvicorn', 'main:app', '--port', '8000', '--reload'], { cwd: path.join(__dirname, '../backend'), stdio: 'inherit', shell: false, }); backendProc.on('error', err => console.error('Failed to start backend:', err)); } else { console.log('Skipping backend spawn (SKIP_ELECTRON_BACKEND set)'); } createWindow(); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); cleanup(); }); process.on('SIGINT', () => { cleanup(); process.exit(0); }); process.on('SIGTERM', () => { cleanup(); process.exit(0); }); EOF cd src cat > App.jsx <<'EOF' import React, { useEffect, useState } from "react"; import axios from "axios"; const API_URL = "http://localhost:8000"; function App() { const [allLikes, setAllLikes] = useState([]); const [wsMsg, setWsMsg] = useState(""); const [error, setError] = useState(""); const [wsAttempts, setWsAttempts] = useState(0); useEffect(() => { axios.get(API_URL + "/likes/all") .then(res => setAllLikes(res.data)) .catch(err => setError(err.message)); let socket; let retryTimer; const connectWs = (attempt = 0) => { setWsAttempts(attempt + 1); socket = new WebSocket("ws://localhost:8000/ws"); socket.onopen = () => { setError(""); }; socket.onmessage = e => { const msg = JSON.parse(e.data); setWsMsg(JSON.stringify(msg)); }; socket.onerror = () => { setError("WebSocket connection failed"); }; socket.onclose = () => { if (attempt < 4) { retryTimer = setTimeout(() => connectWs(attempt + 1), 1000 * (attempt + 1)); } }; }; connectWs(); return () => { if (retryTimer) clearTimeout(retryTimer); if (socket) socket.close(); }; }, []); return (

React · FastAPI · SQLite · WebSocket · ESLint

{error &&

Error: {error}

}

Likes from backend:

    {allLikes.map(t => (
  • {t.title} – {t.artist}
  • ))}

WebSocket (attempt {wsAttempts}): {wsMsg}

); } export default App; EOF cat > main.jsx <<'EOF' import React from 'react' import { createRoot } from 'react-dom/client' import App from './App.jsx' createRoot(document.getElementById('root')).render( , ) EOF cat > index.css <<'EOF' body { font-family: sans-serif; margin: 2em; background: #202126; color: #fafafa; } h1 { color: #6bf; } EOF cd ../ # --- Patch package.json for Electron & deps --- PROJ_NAME="$PROJ" node -e ' const fs = require("fs"); const pkg = JSON.parse(fs.readFileSync("./package.json", "utf8")); pkg.name = process.env.PROJ_NAME || pkg.name; pkg.main = "./electron.cjs"; pkg.scripts = { ...pkg.scripts, electron: "electron .", "electron-build": "npm run build && electron ." }; pkg.dependencies = pkg.dependencies || {}; pkg.devDependencies = pkg.devDependencies || {}; pkg.dependencies.axios = pkg.dependencies.axios || "latest"; pkg.devDependencies.electron = pkg.devDependencies.electron || "latest"; pkg.devDependencies["eslint-plugin-react"] = pkg.devDependencies["eslint-plugin-react"] || "latest"; fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2)); ' cd ../.. cat > "$PROJ/.gitignore" <<'EOF' backend/.venv backend/__pycache__/ backend/appdata.db frontend/node_modules frontend/dist .DS_Store *.pyc EOF cat > "$PROJ/run.sh" <<'EOF' #!/bin/bash set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BACKEND="$ROOT/backend" FRONTEND="$ROOT/frontend" VENV="$BACKEND/.venv" APP_TITLE="${APP_TITLE:-$(basename "$ROOT")}" cleanup() { [[ -n "${BACK_PID:-}" ]] && kill "$BACK_PID" 2>/dev/null || true [[ -n "${FRONT_PID:-}" ]] && kill "$FRONT_PID" 2>/dev/null || true [[ -n "${ELECTRON_PID:-}" ]] && kill "$ELECTRON_PID" 2>/dev/null || true } trap cleanup EXIT INT TERM echo ">> Backend: ensure venv and deps" python3 -m venv "$VENV" source "$VENV/bin/activate" pip install --upgrade pip pip install -r "$BACKEND/requirements.txt" echo ">> Starting backend (uvicorn)..." cd "$BACKEND" python -m uvicorn main:app --port 8000 --reload & BACK_PID=$! echo ">> Installing frontend deps (npm install)..." cd "$FRONTEND" npm install echo ">> Starting frontend (Vite dev server)..." npm run dev -- --host & FRONT_PID=$! echo ">> Starting Electron (UI)..." SKIP_ELECTRON_BACKEND=1 SKIP_ELECTRON_FRONTEND=1 ELECTRON_DEV=true FRONTEND_URL=http://localhost:5173 APP_TITLE="$APP_TITLE" npm run electron & ELECTRON_PID=$! echo ">> Backend PID: $BACK_PID Frontend PID: $FRONT_PID Electron PID: $ELECTRON_PID" wait $BACK_PID $FRONT_PID $ELECTRON_PID EOF chmod +x "$PROJ/run.sh" cat > "$PROJ/README.md" <> Creating Python venv and installing backend requirements..." cd "$PROJ/backend" python3 -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install -r requirements.txt deactivate echo ">> Installing frontend npm dependencies..." cd ../frontend npm install echo ">> All dependencies installed." cd ../.. echo ">> Boilerplate created at '$PROJ'. See $PROJ/README.md for next steps." echo ">> Launching Electron app..." cd "$PROJ/frontend" APP_TITLE="$PROJ" npm run electron