Convert Windows app to Electron and add Android Capacitor app with CI builds

This commit is contained in:
southseact-3d
2026-02-16 09:40:31 +00:00
parent 14f59c2f56
commit ca4bc9184d
13 changed files with 766 additions and 84 deletions

View File

@@ -0,0 +1,229 @@
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);
}
);
});
});