feat: Electron desktop app + onboarding wizard
- electron/main.ts: native window, tray icon, gateway detection, IPC handlers - electron/preload.ts: secure IPC bridge for renderer - electron/onboarding/index.html: 3-step setup wizard - Connect Existing Gateway (auto-detect localhost:18789) - Local New Install (npm install -g openclaw in background) - Cloud (waitlist for managed hosting) - electron-builder.config.js: Mac/Win/Linux packaging, GitHub auto-update - package.json: electron scripts (electron:dev, electron:build:mac/win)
This commit is contained in:
215
electron/main.ts
Normal file
215
electron/main.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* ClawSuite Electron Main Process
|
||||
* Wraps the Vite-built web app in a native desktop window
|
||||
*/
|
||||
|
||||
import { app, BrowserWindow, shell, Menu, Tray, nativeImage, ipcMain } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { existsSync } from 'fs'
|
||||
import { spawn, execSync } from 'child_process'
|
||||
|
||||
// Prevent multiple instances
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
if (!gotTheLock) {
|
||||
app.quit()
|
||||
}
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let tray: Tray | null = null
|
||||
let gatewayProcess: ReturnType<typeof spawn> | null = null
|
||||
|
||||
// Gateway detection
|
||||
const DEFAULT_GATEWAY_PORT = 18789
|
||||
const DEV_PORT = 3000
|
||||
|
||||
function getGatewayUrl(): string | null {
|
||||
try {
|
||||
// Check if gateway is already running
|
||||
execSync(`curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:${DEFAULT_GATEWAY_PORT}/api/health`, {
|
||||
timeout: 3000,
|
||||
})
|
||||
return `http://127.0.0.1:${DEFAULT_GATEWAY_PORT}`
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function isOpenClawInstalled(): boolean {
|
||||
try {
|
||||
execSync('which openclaw || where openclaw', { timeout: 5000 })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function getAppUrl(): string {
|
||||
// In dev, use Vite dev server
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `http://localhost:${DEV_PORT}`
|
||||
}
|
||||
// In production, serve the built files
|
||||
return `file://${join(__dirname, '../dist/client/index.html')}`
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const iconPath = join(__dirname, '../assets/icon.png')
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1400,
|
||||
height: 900,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
title: 'ClawSuite',
|
||||
icon: existsSync(iconPath) ? iconPath : undefined,
|
||||
titleBarStyle: 'hiddenInset', // macOS native title bar
|
||||
trafficLightPosition: { x: 16, y: 16 },
|
||||
backgroundColor: '#0a0a0f',
|
||||
show: false, // Show after ready-to-show
|
||||
webPreferences: {
|
||||
preload: join(__dirname, 'preload.js'),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Graceful show
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow?.show()
|
||||
mainWindow?.focus()
|
||||
})
|
||||
|
||||
// Check if we need onboarding or go straight to dashboard
|
||||
const gatewayUrl = getGatewayUrl()
|
||||
|
||||
if (gatewayUrl) {
|
||||
// Gateway found — load the app directly
|
||||
const appUrl = getAppUrl()
|
||||
mainWindow.loadURL(appUrl)
|
||||
} else {
|
||||
// No gateway — show onboarding wizard
|
||||
mainWindow.loadFile(join(__dirname, '../electron/onboarding/index.html'))
|
||||
}
|
||||
|
||||
// Open external links in default browser
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url.startsWith('http')) {
|
||||
shell.openExternal(url)
|
||||
}
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null
|
||||
})
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
const iconPath = join(__dirname, '../assets/tray-icon.png')
|
||||
if (!existsSync(iconPath)) return
|
||||
|
||||
tray = new Tray(nativeImage.createFromPath(iconPath))
|
||||
tray.setToolTip('ClawSuite')
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Open ClawSuite', click: () => mainWindow?.show() },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Gateway Status', enabled: false },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', click: () => app.quit() },
|
||||
])
|
||||
|
||||
tray.setContextMenu(contextMenu)
|
||||
tray.on('click', () => mainWindow?.show())
|
||||
}
|
||||
|
||||
// IPC handlers for onboarding wizard
|
||||
ipcMain.handle('gateway:check', () => {
|
||||
return { url: getGatewayUrl(), installed: isOpenClawInstalled() }
|
||||
})
|
||||
|
||||
ipcMain.handle('gateway:install', async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const install = spawn('npm', ['install', '-g', 'openclaw'], {
|
||||
shell: true,
|
||||
stdio: 'pipe',
|
||||
})
|
||||
|
||||
let output = ''
|
||||
install.stdout?.on('data', (data) => { output += data.toString() })
|
||||
install.stderr?.on('data', (data) => { output += data.toString() })
|
||||
|
||||
install.on('close', (code) => {
|
||||
if (code === 0) resolve({ success: true, output })
|
||||
else reject(new Error(`Install failed with code ${code}: ${output}`))
|
||||
})
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('gateway:start', async () => {
|
||||
return new Promise((resolve) => {
|
||||
gatewayProcess = spawn('openclaw', ['gateway', 'start'], {
|
||||
shell: true,
|
||||
stdio: 'pipe',
|
||||
detached: true,
|
||||
})
|
||||
|
||||
// Give it a few seconds to boot
|
||||
setTimeout(() => {
|
||||
const url = getGatewayUrl()
|
||||
resolve({ success: !!url, url })
|
||||
}, 5000)
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('gateway:connect', async (_event, url: string) => {
|
||||
try {
|
||||
execSync(`curl -s -o /dev/null -w "%{http_code}" ${url}/api/health`, { timeout: 3000 })
|
||||
return { success: true, url }
|
||||
} catch {
|
||||
return { success: false, error: 'Could not connect to gateway' }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('onboarding:complete', async (_event, config: { mode: string; gatewayUrl: string }) => {
|
||||
// Store config and load the main app
|
||||
if (mainWindow) {
|
||||
const appUrl = getAppUrl()
|
||||
// Pass gateway URL as query param
|
||||
const url = new URL(appUrl)
|
||||
url.searchParams.set('gateway', config.gatewayUrl)
|
||||
mainWindow.loadURL(url.toString())
|
||||
}
|
||||
})
|
||||
|
||||
// App lifecycle
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
createTray()
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('before-quit', () => {
|
||||
// Don't kill gateway — it should persist
|
||||
tray?.destroy()
|
||||
})
|
||||
|
||||
// Set app name
|
||||
app.setName('ClawSuite')
|
||||
533
electron/onboarding/index.html
Normal file
533
electron/onboarding/index.html
Normal file
@@ -0,0 +1,533 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ClawSuite — Setup</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--bg: #0a0a0f;
|
||||
--surface: #12121a;
|
||||
--surface-hover: #1a1a25;
|
||||
--border: #1e1e2e;
|
||||
--text: #e4e4e7;
|
||||
--text-muted: #71717a;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #818cf8;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
--radius: 12px;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
-webkit-app-region: drag;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wizard {
|
||||
width: 520px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.logo h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.logo h1 span { font-weight: 400; opacity: 0.7; }
|
||||
|
||||
.logo p {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Steps */
|
||||
.step { display: none; animation: fadeIn 0.3s ease; }
|
||||
.step.active { display: block; }
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Option cards */
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.option {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: var(--surface-hover);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.option.selected {
|
||||
border-color: var(--accent);
|
||||
background: rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
font-size: 28px;
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.option-content h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.option-content p {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.badge-free { background: rgba(34, 197, 94, 0.15); color: var(--success); }
|
||||
.badge-pro { background: rgba(99, 102, 241, 0.15); color: var(--accent); }
|
||||
.badge-recommended { background: rgba(245, 158, 11, 0.15); color: var(--warning); }
|
||||
|
||||
/* Input */
|
||||
.input-group {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.input-group input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.input-group input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 32px;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover { background: var(--surface-hover); }
|
||||
|
||||
/* Progress bar */
|
||||
.progress {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.progress-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--border);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.progress-dot.active { background: var(--accent); width: 24px; border-radius: 4px; }
|
||||
.progress-dot.done { background: var(--success); }
|
||||
|
||||
/* Status messages */
|
||||
.status {
|
||||
margin-top: 16px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status.show { display: flex; align-items: center; gap: 8px; }
|
||||
.status-loading { background: rgba(99, 102, 241, 0.1); color: var(--accent); }
|
||||
.status-success { background: rgba(34, 197, 94, 0.1); color: var(--success); }
|
||||
.status-error { background: rgba(239, 68, 68, 0.1); color: var(--error); }
|
||||
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Terminal output */
|
||||
.terminal {
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
color: #22c55e;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.terminal.show { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wizard">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" rx="16" fill="#6366f1"/>
|
||||
<path d="M20 44V20h8l8 12 8-12h0v24" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="32" cy="32" r="4" fill="white" opacity="0.6"/>
|
||||
</svg>
|
||||
<h1>Claw<span>Suite</span></h1>
|
||||
<p>Your AI command center</p>
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-dot active" id="dot-1"></div>
|
||||
<div class="progress-dot" id="dot-2"></div>
|
||||
<div class="progress-dot" id="dot-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Choose Setup Mode -->
|
||||
<div class="step active" id="step-1">
|
||||
<div class="options">
|
||||
<div class="option" onclick="selectMode('existing')">
|
||||
<div class="option-icon">🔗</div>
|
||||
<div class="option-content">
|
||||
<h3>Connect Existing Gateway <span class="badge badge-recommended">Recommended</span></h3>
|
||||
<p>Already have OpenClaw running? Connect to your local or remote gateway instantly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option" onclick="selectMode('local')">
|
||||
<div class="option-icon">🖥️</div>
|
||||
<div class="option-content">
|
||||
<h3>Local — New Install <span class="badge badge-free">Free</span></h3>
|
||||
<p>Install OpenClaw on this machine. Full control, self-hosted, runs locally.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option" onclick="selectMode('cloud')">
|
||||
<div class="option-icon">☁️</div>
|
||||
<div class="option-content">
|
||||
<h3>Cloud <span class="badge badge-pro">Pro</span></h3>
|
||||
<p>Managed hosting. We run the gateway — you just build. Starting at $20/mo.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Configure -->
|
||||
<div class="step" id="step-2">
|
||||
<!-- Content injected by JS based on mode -->
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Ready -->
|
||||
<div class="step" id="step-3">
|
||||
<div style="text-align: center; padding: 20px 0;">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">🚀</div>
|
||||
<h2 style="font-size: 22px; margin-bottom: 8px;">You're all set!</h2>
|
||||
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;">
|
||||
ClawSuite is connected and ready to go.
|
||||
</p>
|
||||
<div id="connection-info" style="background: var(--surface); border-radius: 8px; padding: 16px; text-align: left; font-size: 13px;">
|
||||
<!-- Filled by JS -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" onclick="finishSetup()">Open ClawSuite →</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedMode = null
|
||||
let gatewayUrl = null
|
||||
|
||||
function selectMode(mode) {
|
||||
selectedMode = mode
|
||||
document.querySelectorAll('.option').forEach(o => o.classList.remove('selected'))
|
||||
event.currentTarget.classList.add('selected')
|
||||
|
||||
setTimeout(() => goToStep(2), 300)
|
||||
}
|
||||
|
||||
function goToStep(step) {
|
||||
document.querySelectorAll('.step').forEach(s => s.classList.remove('active'))
|
||||
document.getElementById(`step-${step}`).classList.add('active')
|
||||
|
||||
document.querySelectorAll('.progress-dot').forEach((d, i) => {
|
||||
d.classList.remove('active', 'done')
|
||||
if (i + 1 < step) d.classList.add('done')
|
||||
if (i + 1 === step) d.classList.add('active')
|
||||
})
|
||||
|
||||
if (step === 2) renderStep2()
|
||||
}
|
||||
|
||||
function renderStep2() {
|
||||
const container = document.getElementById('step-2')
|
||||
|
||||
if (selectedMode === 'existing') {
|
||||
container.innerHTML = `
|
||||
<h2 style="font-size: 18px; margin-bottom: 4px;">Connect to Gateway</h2>
|
||||
<p style="color: var(--text-muted); font-size: 13px;">Enter your OpenClaw gateway URL</p>
|
||||
<div class="input-group">
|
||||
<label>Gateway URL</label>
|
||||
<input type="text" id="gateway-url" placeholder="http://localhost:18789" value="http://localhost:18789" />
|
||||
</div>
|
||||
<div class="status" id="connect-status"></div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="goToStep(1)">← Back</button>
|
||||
<button class="btn btn-primary" onclick="connectGateway()">Connect</button>
|
||||
</div>
|
||||
`
|
||||
// Auto-detect
|
||||
detectGateway()
|
||||
} else if (selectedMode === 'local') {
|
||||
container.innerHTML = `
|
||||
<h2 style="font-size: 18px; margin-bottom: 4px;">Install OpenClaw</h2>
|
||||
<p style="color: var(--text-muted); font-size: 13px;">We'll install OpenClaw and start the gateway</p>
|
||||
<div class="status" id="install-status"></div>
|
||||
<div class="terminal" id="install-terminal"></div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="goToStep(1)">← Back</button>
|
||||
<button class="btn btn-primary" id="install-btn" onclick="installOpenClaw()">Install & Start</button>
|
||||
</div>
|
||||
`
|
||||
} else if (selectedMode === 'cloud') {
|
||||
container.innerHTML = `
|
||||
<h2 style="font-size: 18px; margin-bottom: 4px;">Cloud Setup</h2>
|
||||
<p style="color: var(--text-muted); font-size: 13px;">Create your account to get started</p>
|
||||
<div class="input-group">
|
||||
<label>Email</label>
|
||||
<input type="email" id="cloud-email" placeholder="you@example.com" />
|
||||
</div>
|
||||
<div class="status show status-loading" style="margin-top: 16px;">
|
||||
<span>☁️</span>
|
||||
<span>Cloud hosting is coming soon. Join the waitlist!</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="btn btn-secondary" onclick="goToStep(1)">← Back</button>
|
||||
<button class="btn btn-primary" onclick="joinWaitlist()">Join Waitlist</button>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
async function detectGateway() {
|
||||
const status = document.getElementById('connect-status')
|
||||
status.className = 'status show status-loading'
|
||||
status.innerHTML = '<div class="spinner"></div> Checking for gateway...'
|
||||
|
||||
try {
|
||||
const result = await window.clawsuite.gateway.check()
|
||||
if (result.url) {
|
||||
document.getElementById('gateway-url').value = result.url
|
||||
status.className = 'status show status-success'
|
||||
status.innerHTML = '✓ Gateway detected at ' + result.url
|
||||
} else {
|
||||
status.className = 'status show status-loading'
|
||||
status.innerHTML = '⚠️ No gateway detected. Enter URL manually or go back and choose "New Install".'
|
||||
}
|
||||
} catch (e) {
|
||||
status.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
async function connectGateway() {
|
||||
const url = document.getElementById('gateway-url').value.trim()
|
||||
const status = document.getElementById('connect-status')
|
||||
|
||||
status.className = 'status show status-loading'
|
||||
status.innerHTML = '<div class="spinner"></div> Connecting...'
|
||||
|
||||
try {
|
||||
const result = await window.clawsuite.gateway.connect(url)
|
||||
if (result.success) {
|
||||
gatewayUrl = url
|
||||
status.className = 'status show status-success'
|
||||
status.innerHTML = '✓ Connected!'
|
||||
setTimeout(() => {
|
||||
document.getElementById('connection-info').innerHTML = `
|
||||
<div><strong>Mode:</strong> Local Gateway</div>
|
||||
<div style="margin-top: 8px;"><strong>URL:</strong> ${gatewayUrl}</div>
|
||||
<div style="margin-top: 8px;"><strong>Status:</strong> <span style="color: var(--success);">● Connected</span></div>
|
||||
`
|
||||
goToStep(3)
|
||||
}, 1000)
|
||||
} else {
|
||||
status.className = 'status show status-error'
|
||||
status.innerHTML = '✗ Could not connect. Check the URL and try again.'
|
||||
}
|
||||
} catch (e) {
|
||||
status.className = 'status show status-error'
|
||||
status.innerHTML = '✗ Connection failed: ' + e.message
|
||||
}
|
||||
}
|
||||
|
||||
async function installOpenClaw() {
|
||||
const status = document.getElementById('install-status')
|
||||
const terminal = document.getElementById('install-terminal')
|
||||
const btn = document.getElementById('install-btn')
|
||||
|
||||
btn.disabled = true
|
||||
btn.textContent = 'Installing...'
|
||||
status.className = 'status show status-loading'
|
||||
status.innerHTML = '<div class="spinner"></div> Installing OpenClaw...'
|
||||
terminal.classList.add('show')
|
||||
terminal.textContent = '$ npm install -g openclaw\n'
|
||||
|
||||
try {
|
||||
const result = await window.clawsuite.gateway.install()
|
||||
terminal.textContent += result.output + '\n✓ Installed!\n\n'
|
||||
|
||||
status.innerHTML = '<div class="spinner"></div> Starting gateway...'
|
||||
terminal.textContent += '$ openclaw gateway start\n'
|
||||
|
||||
const startResult = await window.clawsuite.gateway.start()
|
||||
if (startResult.success) {
|
||||
gatewayUrl = startResult.url
|
||||
status.className = 'status show status-success'
|
||||
status.innerHTML = '✓ Gateway running!'
|
||||
terminal.textContent += '✓ Gateway started at ' + gatewayUrl + '\n'
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('connection-info').innerHTML = `
|
||||
<div><strong>Mode:</strong> Local (Fresh Install)</div>
|
||||
<div style="margin-top: 8px;"><strong>URL:</strong> ${gatewayUrl}</div>
|
||||
<div style="margin-top: 8px;"><strong>Status:</strong> <span style="color: var(--success);">● Running</span></div>
|
||||
`
|
||||
goToStep(3)
|
||||
}, 1500)
|
||||
}
|
||||
} catch (e) {
|
||||
status.className = 'status show status-error'
|
||||
status.innerHTML = '✗ Installation failed. Try running manually: npm install -g openclaw'
|
||||
terminal.textContent += '✗ Error: ' + e.message + '\n'
|
||||
btn.disabled = false
|
||||
btn.textContent = 'Retry'
|
||||
}
|
||||
}
|
||||
|
||||
async function joinWaitlist() {
|
||||
const email = document.getElementById('cloud-email').value.trim()
|
||||
if (!email) return
|
||||
|
||||
// TODO: Submit to waitlist API
|
||||
const status = document.querySelector('#step-2 .status')
|
||||
status.className = 'status show status-success'
|
||||
status.innerHTML = '✓ You\'re on the list! We\'ll email you when Cloud is ready.'
|
||||
}
|
||||
|
||||
async function finishSetup() {
|
||||
if (gatewayUrl) {
|
||||
await window.clawsuite.onboarding.complete({
|
||||
mode: selectedMode,
|
||||
gatewayUrl: gatewayUrl,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
29
electron/preload.ts
Normal file
29
electron/preload.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* ClawSuite Electron Preload Script
|
||||
* Exposes safe IPC bridge to renderer
|
||||
*/
|
||||
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
contextBridge.exposeInMainWorld('clawsuite', {
|
||||
// Gateway management
|
||||
gateway: {
|
||||
check: () => ipcRenderer.invoke('gateway:check'),
|
||||
install: () => ipcRenderer.invoke('gateway:install'),
|
||||
start: () => ipcRenderer.invoke('gateway:start'),
|
||||
connect: (url: string) => ipcRenderer.invoke('gateway:connect', url),
|
||||
},
|
||||
|
||||
// Onboarding
|
||||
onboarding: {
|
||||
complete: (config: { mode: string; gatewayUrl: string }) =>
|
||||
ipcRenderer.invoke('onboarding:complete', config),
|
||||
},
|
||||
|
||||
// App info
|
||||
app: {
|
||||
version: process.env.npm_package_version || '3.2.0',
|
||||
platform: process.platform,
|
||||
isElectron: true,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user