const { app, BrowserWindow, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs'); const https = require('https'); const http = require('http'); const { exec } = require('child_process'); const BACKEND_BASE_URL = process.env.BACKEND_BASE_URL || 'https://api.example.com'; let mainWindow; function getAppDataPath() { return app.getPath('userData'); } function getAppsDir() { const dir = path.join(getAppDataPath(), 'apps'); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } return dir; } function getSecretsPath() { const dir = path.join(getAppDataPath(), 'secrets'); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } return path.join(dir, 'secure.json'); } function loadSecureStore() { const secretsPath = getSecretsPath(); if (!fs.existsSync(secretsPath)) { return { data: {} }; } try { const content = fs.readFileSync(secretsPath, 'utf8'); return JSON.parse(content); } catch { return { data: {} }; } } function saveSecureStore(store) { const secretsPath = getSecretsPath(); fs.writeFileSync(secretsPath, JSON.stringify(store, null, 2), 'utf8'); } function getOpenCodeBinaryPath() { const base = path.join(getAppDataPath(), 'opencode'); if (!fs.existsSync(base)) { fs.mkdirSync(base, { recursive: true }); } const name = process.platform === 'win32' ? 'opencode.exe' : 'opencode'; return path.join(base, name); } function createWindow() { mainWindow = new BrowserWindow({ width: 1280, height: 800, webPreferences: { preload: path.join(__dirname, 'electron-preload.js'), contextIsolation: true, nodeIntegration: false, }, title: 'ShopifyAI Desktop', show: false, }); mainWindow.once('ready-to-show', () => { mainWindow.show(); }); const uiDistPath = path.join(__dirname, 'ui-dist'); const indexPath = path.join(uiDistPath, 'index.html'); if (fs.existsSync(indexPath)) { mainWindow.loadFile(indexPath); } else { mainWindow.loadURL('http://localhost:3000'); } } 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('save-api-key', async (event, token) => { if (!token || typeof token !== 'string' || token.trim() === '') { throw new Error('token is required'); } const store = loadSecureStore(); store.data['opencode_api_key'] = token; saveSecureStore(store); return { success: true }; }); ipcMain.handle('persist-app', async (event, appData) => { if (!appData || typeof appData !== 'object') { throw new Error('App payload must be an object'); } if (!appData.id || appData.id.trim() === '') { throw new Error('app.id is required'); } const dir = getAppsDir(); const appPath = path.join(dir, `${appData.id}.json`); fs.writeFileSync(appPath, JSON.stringify(appData, null, 2), 'utf8'); return { success: true }; }); ipcMain.handle('list-apps', async () => { const dir = getAppsDir(); const apps = []; if (!fs.existsSync(dir)) { return apps; } const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && entry.name.endsWith('.json')) { const appPath = path.join(dir, entry.name); try { const content = fs.readFileSync(appPath, 'utf8'); apps.push(JSON.parse(content)); } catch {} } } return apps; }); ipcMain.handle('sync-app', async (event, appId) => { if (!appId || appId.trim() === '') { throw new Error('appId is required'); } const dir = getAppsDir(); const appPath = path.join(dir, `${appId}.json`); if (!fs.existsSync(appPath)) { throw new Error(`App not found: ${appId}`); } const appData = JSON.parse(fs.readFileSync(appPath, 'utf8')); const url = `${BACKEND_BASE_URL}/desktop/apps/sync`; return new Promise((resolve, reject) => { const client = url.startsWith('https') ? https : http; const postData = JSON.stringify(appData); const req = client.request(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData), }, }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { if (res.statusCode >= 200 && res.statusCode < 300) { resolve({ success: true }); } else { reject(new Error(`Sync failed: ${res.statusCode} ${data}`)); } }); }); req.on('error', (err) => reject(new Error(`Sync request failed: ${err.message}`))); req.write(postData); req.end(); }); }); ipcMain.handle('run-opencode-task', async (event, appId, taskName, args = []) => { if (!appId || appId.trim() === '') { throw new Error('appId is required'); } if (!taskName || taskName.trim() === '') { throw new Error('taskName is required'); } const binaryPath = getOpenCodeBinaryPath(); if (!fs.existsSync(binaryPath)) { throw new Error('OpenCode binary not found on device'); } const store = loadSecureStore(); const apiKey = store.data['opencode_api_key']; if (!apiKey) { throw new Error('API key not set'); } return new Promise((resolve, reject) => { const allArgs = [taskName, ...args]; const child = exec( `"${binaryPath}" ${allArgs.map(a => `"${a}"`).join(' ')}`, { env: { ...process.env, OPENCODE_API_KEY: apiKey, APP_ID: appId, }, }, (error, stdout, stderr) => { if (error) { reject(new Error(`OpenCode exited with error: ${stderr || error.message}`)); return; } resolve(stdout); } ); }); });