230 lines
5.8 KiB
JavaScript
230 lines
5.8 KiB
JavaScript
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);
|
|
}
|
|
);
|
|
});
|
|
});
|