diff --git a/.github/workflows/build-android-app.yml b/.github/workflows/build-android-app.yml
new file mode 100644
index 0000000..ca8693a
--- /dev/null
+++ b/.github/workflows/build-android-app.yml
@@ -0,0 +1,101 @@
+name: Build Android App (Capacitor)
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ paths:
+ - android-app/**
+ - chat/public/**
+ - .github/workflows/build-android-app.yml
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ actions: read
+
+jobs:
+ build-android:
+ name: Build Android APK
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+ cache-dependency-path: android-app/package-lock.json
+
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v3
+
+ - name: Install dependencies
+ run: npm install
+ working-directory: android-app
+
+ - name: Prepare UI
+ run: npm run prepare-ui
+ working-directory: android-app
+
+ - name: Initialize Capacitor
+ run: npx cap init "ShopifyAI" com.shopifyai.app --web-dir www
+ working-directory: android-app
+
+ - name: Add Android platform
+ run: npx cap add android
+ working-directory: android-app
+
+ - name: Sync Capacitor
+ run: npx cap sync android
+ working-directory: android-app
+
+ - name: Build APK
+ run: |
+ cd android
+ chmod +x gradlew
+ ./gradlew assembleDebug
+ working-directory: android-app
+ env:
+ BACKEND_BASE_URL: ${{ secrets.BACKEND_BASE_URL }}
+
+ - name: Upload APK artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: shopify-ai-android-apk
+ path: android-app/android/app/build/outputs/apk/debug/*.apk
+ retention-days: 7
+
+ - name: Create Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: android-app-${{ github.sha }}
+ release_name: ShopifyAI Android App ${{ github.sha }}
+ draft: false
+ prerelease: false
+
+ - name: Get APK filename
+ id: apk
+ run: echo "filename=$(ls android-app/android/app/build/outputs/apk/debug/*.apk)" >> $GITHUB_OUTPUT
+
+ - name: Upload Release Asset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ asset_path: android-app/android/app/build/outputs/apk/debug/app-debug.apk
+ asset_name: ShopifyAI-0.1.0.apk
+ asset_content_type: application/vnd.android.package-archive
diff --git a/.github/workflows/build-electron-app.yml b/.github/workflows/build-electron-app.yml
new file mode 100644
index 0000000..2753f3a
--- /dev/null
+++ b/.github/workflows/build-electron-app.yml
@@ -0,0 +1,69 @@
+name: Build Electron Windows App
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ paths:
+ - windows-app/**
+ - chat/public/**
+ - .github/workflows/build-electron-app.yml
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ actions: read
+
+jobs:
+ build-windows:
+ name: Build Electron App (Windows)
+ runs-on: windows-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+ cache-dependency-path: windows-app/package-lock.json
+
+ - name: Install dependencies
+ run: npm install
+ working-directory: windows-app
+
+ - name: Build Electron app
+ run: npm run build:win
+ working-directory: windows-app
+ env:
+ BACKEND_BASE_URL: ${{ secrets.BACKEND_BASE_URL }}
+
+ - name: Upload Windows artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: shopify-ai-electron-windows
+ path: windows-app/dist/*.exe
+ retention-days: 7
+
+ - name: Create Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: electron-app-${{ github.sha }}
+ release_name: ShopifyAI Electron App ${{ github.sha }}
+ draft: false
+ prerelease: false
+
+ - name: Upload Release Asset
+ uses: actions/upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ asset_path: windows-app/dist/ShopifyAI Desktop Setup 0.1.0.exe
+ asset_name: ShopifyAI-Desktop-Setup-0.1.0.exe
+ asset_content_type: application/octet-stream
diff --git a/android-app/.gitignore b/android-app/.gitignore
new file mode 100644
index 0000000..b61ce95
--- /dev/null
+++ b/android-app/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+www/
+android/
+*.apk
+*.aab
+.gradle/
+.idea/
+*.iml
+.DS_Store
diff --git a/android-app/README.md b/android-app/README.md
new file mode 100644
index 0000000..ab90d7a
--- /dev/null
+++ b/android-app/README.md
@@ -0,0 +1,34 @@
+# Android App (Capacitor)
+
+This folder contains the Android app build for the project. The app reuses the existing web UI (apps and builder screens) and wraps it with Capacitor for native Android deployment.
+
+## Goals
+- Reuse the existing web UI with minimal divergence.
+- Provide a native Android APK for distribution.
+- Sync apps between device and backend.
+- Build via GitHub Actions; do not build locally.
+
+## How it works
+- **UI reuse:** `npm run prepare-ui` copies `../chat/public` into `www` and injects a bridge script so existing pages can talk to native APIs.
+- **Native bridge:** Capacitor provides native access to preferences and HTTP requests.
+- **Local storage:** Apps are saved using Capacitor Preferences plugin, isolated per user.
+- **Syncing:** `syncApp` posts the locally saved app JSON to the backend.
+
+## Setup
+1. Install prerequisites (Node 18+, Android SDK).
+2. From this folder: `npm install`.
+3. Pull UI assets: `npm run prepare-ui`.
+4. Add Android platform: `npx cap add android`.
+5. Sync: `npx cap sync android`.
+
+## Development
+- `npm run build` prepares UI and syncs with Android.
+- `npx cap open android` opens Android Studio for development.
+
+## CI build
+- GitHub Actions workflow: `.github/workflows/build-android-app.yml`.
+- The action runs on `ubuntu-latest`, sets up Java/Android SDK, and builds the APK.
+
+## Security notes
+- API keys are stored in Capacitor Preferences (encrypted on Android).
+- OpenCode execution is not supported on mobile (desktop app only).
diff --git a/android-app/capacitor-bridge.js b/android-app/capacitor-bridge.js
new file mode 100644
index 0000000..0f284e3
--- /dev/null
+++ b/android-app/capacitor-bridge.js
@@ -0,0 +1,45 @@
+import { Preferences } from '@capacitor/preferences';
+import { Http } from '@capacitor/http';
+
+const BACKEND_BASE_URL = window.BACKEND_BASE_URL || 'https://api.example.com';
+
+export { Preferences };
+
+export async function syncApp(appId) {
+ if (!appId || appId.trim() === '') {
+ throw new Error('appId is required');
+ }
+
+ const { value } = await Preferences.get({ key: `app_${appId}` });
+ if (!value) {
+ throw new Error(`App not found: ${appId}`);
+ }
+
+ const appData = JSON.parse(value);
+
+ const response = await Http.request({
+ method: 'POST',
+ url: `${BACKEND_BASE_URL}/desktop/apps/sync`,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ data: appData,
+ });
+
+ if (response.status < 200 || response.status >= 300) {
+ throw new Error(`Sync failed: ${response.status}`);
+ }
+
+ return response.data;
+}
+
+export async function runOpencodeTask(appId, taskName, args = []) {
+ if (!appId || appId.trim() === '') {
+ throw new Error('appId is required');
+ }
+ if (!taskName || taskName.trim() === '') {
+ throw new Error('taskName is required');
+ }
+
+ throw new Error('OpenCode execution is not supported on mobile devices. Please use the desktop app for this feature.');
+}
diff --git a/android-app/capacitor.config.json b/android-app/capacitor.config.json
new file mode 100644
index 0000000..6341428
--- /dev/null
+++ b/android-app/capacitor.config.json
@@ -0,0 +1,21 @@
+{
+ "appId": "com.shopifyai.app",
+ "appName": "ShopifyAI",
+ "webDir": "www",
+ "server": {
+ "androidScheme": "https"
+ },
+ "plugins": {
+ "Http": {
+ "enabled": true
+ },
+ "Preferences": {
+ "group": "ShopifyAI"
+ }
+ },
+ "android": {
+ "buildOptions": {
+ "releaseType": "APK"
+ }
+ }
+}
diff --git a/android-app/package.json b/android-app/package.json
new file mode 100644
index 0000000..3a57e82
--- /dev/null
+++ b/android-app/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "shopify-ai-android-app",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "prepare-ui": "node ./scripts/sync-ui.js",
+ "build": "npm run prepare-ui && npx cap sync android",
+ "cap:init": "npx cap init 'ShopifyAI' com.shopifyai.app --web-dir www",
+ "cap:add": "npx cap add android",
+ "cap:sync": "npx cap sync android",
+ "cap:open": "npx cap open android",
+ "android:build": "npm run build && cd android && ./gradlew assembleDebug"
+ },
+ "dependencies": {
+ "@capacitor/android": "^5.6.0",
+ "@capacitor/core": "^5.6.0",
+ "@capacitor/preferences": "^5.0.7",
+ "@capacitor/http": "^5.0.7"
+ },
+ "devDependencies": {
+ "@capacitor/cli": "^5.6.0",
+ "fs-extra": "^11.2.0"
+ }
+}
diff --git a/android-app/scripts/sync-ui.js b/android-app/scripts/sync-ui.js
new file mode 100644
index 0000000..296ee5a
--- /dev/null
+++ b/android-app/scripts/sync-ui.js
@@ -0,0 +1,95 @@
+import fs from "fs-extra";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+const root = path.resolve(__dirname, "..", "..");
+const source = path.join(root, "chat", "public");
+const dest = path.resolve(__dirname, "..", "www");
+
+async function copyUi() {
+ if (!(await fs.pathExists(source))) {
+ throw new Error(`Source UI folder not found: ${source}`);
+ }
+
+ await fs.emptyDir(dest);
+ await fs.copy(source, dest, {
+ filter: (src) => !src.endsWith(".map") && !src.includes(".DS_Store"),
+ overwrite: true,
+ });
+}
+
+async function findHtmlFiles(dir) {
+ const entries = await fs.readdir(dir, { withFileTypes: true });
+ const files = await Promise.all(
+ entries.map(async (entry) => {
+ const full = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ return findHtmlFiles(full);
+ }
+ return entry.isFile() && entry.name.endsWith(".html") ? [full] : [];
+ })
+ );
+ return files.flat();
+}
+
+async function injectBridge() {
+ const bridgeContent = `
+
+`;
+
+ const files = await findHtmlFiles(dest);
+ await Promise.all(
+ files.map(async (fullPath) => {
+ const html = await fs.readFile(fullPath, "utf8");
+ if (html.includes("capacitor-bridge.js")) return;
+
+ const injection = bridgeContent + '\n ';
+ const updated = html.replace(/<\/head>/i, injection);
+ await fs.writeFile(fullPath, updated, "utf8");
+ })
+ );
+}
+
+async function main() {
+ await copyUi();
+ await injectBridge();
+ console.log(`UI prepared in ${dest}`);
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/windows-app/electron-bridge.js b/windows-app/electron-bridge.js
new file mode 100644
index 0000000..8efca82
--- /dev/null
+++ b/windows-app/electron-bridge.js
@@ -0,0 +1,33 @@
+async function saveApiKey(token) {
+ if (!token || typeof token !== "string") throw new Error("token is required");
+ return window.windowsAppBridge.saveApiKey(token);
+}
+
+async function persistApp(app) {
+ if (!app || typeof app !== "object") throw new Error("App payload must be an object");
+ if (!app.id) throw new Error("app.id is required");
+ return window.windowsAppBridge.persistApp(app);
+}
+
+async function listApps() {
+ return window.windowsAppBridge.listApps();
+}
+
+async function syncApp(appId) {
+ if (!appId) throw new Error("appId is required");
+ return window.windowsAppBridge.syncApp(appId);
+}
+
+async function runOpencodeTask(appId, taskName, args = []) {
+ if (!appId) throw new Error("appId is required");
+ if (!taskName) throw new Error("taskName is required");
+ return window.windowsAppBridge.runOpencodeTask(appId, taskName, args);
+}
+
+if (window.windowsAppBridge) {
+ window.windowsAppBridge.saveApiKey = saveApiKey;
+ window.windowsAppBridge.persistApp = persistApp;
+ window.windowsAppBridge.listApps = listApps;
+ window.windowsAppBridge.syncApp = syncApp;
+ window.windowsAppBridge.runOpencodeTask = runOpencodeTask;
+}
diff --git a/windows-app/electron-main.js b/windows-app/electron-main.js
new file mode 100644
index 0000000..31e034e
--- /dev/null
+++ b/windows-app/electron-main.js
@@ -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);
+ }
+ );
+ });
+});
diff --git a/windows-app/electron-preload.js b/windows-app/electron-preload.js
new file mode 100644
index 0000000..3c721f8
--- /dev/null
+++ b/windows-app/electron-preload.js
@@ -0,0 +1,10 @@
+const { contextBridge, ipcRenderer } = require('electron');
+
+contextBridge.exposeInMainWorld('windowsAppBridge', {
+ saveApiKey: (token) => ipcRenderer.invoke('save-api-key', token),
+ persistApp: (app) => ipcRenderer.invoke('persist-app', app),
+ listApps: () => ipcRenderer.invoke('list-apps'),
+ syncApp: (appId) => ipcRenderer.invoke('sync-app', appId),
+ runOpencodeTask: (appId, taskName, args) =>
+ ipcRenderer.invoke('run-opencode-task', appId, taskName, args || []),
+});
diff --git a/windows-app/package.json b/windows-app/package.json
index f29ae8e..a4e7400 100644
--- a/windows-app/package.json
+++ b/windows-app/package.json
@@ -1,18 +1,51 @@
-{
- "name": "shopify-ai-windows-app",
- "version": "0.1.0",
- "private": true,
- "type": "module",
- "scripts": {
- "prepare-ui": "node ./scripts/sync-ui.js",
- "dev": "npm run prepare-ui && tauri dev",
- "build": "npm run prepare-ui && tauri build"
- },
- "dependencies": {
- "@tauri-apps/api": "^1.5.4"
- },
- "devDependencies": {
- "@tauri-apps/cli": "^1.5.9",
- "fs-extra": "^11.2.0"
- }
-}
+{
+ "name": "shopify-ai-windows-app",
+ "version": "0.1.0",
+ "private": true,
+ "main": "electron-main.js",
+ "scripts": {
+ "prepare-ui": "node ./scripts/sync-ui.js",
+ "dev": "npm run prepare-ui && electron .",
+ "build": "npm run prepare-ui && electron-builder",
+ "build:win": "npm run prepare-ui && electron-builder --win",
+ "build:mac": "npm run prepare-ui && electron-builder --mac",
+ "build:linux": "npm run prepare-ui && electron-builder --linux"
+ },
+ "dependencies": {},
+ "devDependencies": {
+ "electron": "^28.0.0",
+ "electron-builder": "^24.9.1",
+ "fs-extra": "^11.2.0"
+ },
+ "build": {
+ "appId": "com.shopifyai.desktop",
+ "productName": "ShopifyAI Desktop",
+ "directories": {
+ "output": "dist"
+ },
+ "files": [
+ "electron-main.js",
+ "electron-preload.js",
+ "ui-dist/**/*"
+ ],
+ "win": {
+ "target": [
+ "nsis",
+ "portable"
+ ],
+ "icon": "assets/icon.ico"
+ },
+ "mac": {
+ "target": "dmg",
+ "icon": "assets/icon.icns"
+ },
+ "linux": {
+ "target": "AppImage",
+ "icon": "assets/icon.png"
+ },
+ "nsis": {
+ "oneClick": false,
+ "allowToChangeInstallationDirectory": true
+ }
+ }
+}
diff --git a/windows-app/scripts/sync-ui.js b/windows-app/scripts/sync-ui.js
index 2bdef42..49927a8 100644
--- a/windows-app/scripts/sync-ui.js
+++ b/windows-app/scripts/sync-ui.js
@@ -1,66 +1,45 @@
-import fs from "fs-extra";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-
-const __dirname = path.dirname(fileURLToPath(import.meta.url));
-
-const root = path.resolve(__dirname, "..", "..");
-const source = path.join(root, "chat", "public");
-const dest = path.resolve(__dirname, "..", "ui-dist");
-const bridgeFile = path.resolve(__dirname, "..", "tauri-bridge.js");
-
-async function copyUi() {
- if (!(await fs.pathExists(source))) {
- throw new Error(`Source UI folder not found: ${source}`);
- }
-
- await fs.emptyDir(dest);
- await fs.copy(source, dest, {
- filter: (src) => !src.endsWith(".map") && !src.includes(".DS_Store"),
- overwrite: true,
- });
-
- await fs.copy(bridgeFile, path.join(dest, "tauri-bridge.js"));
-}
-
-async function findHtmlFiles(dir) {
- const entries = await fs.readdir(dir, { withFileTypes: true });
- const files = await Promise.all(
- entries.map(async (entry) => {
- const full = path.join(dir, entry.name);
- if (entry.isDirectory()) {
- return findHtmlFiles(full);
- }
- return entry.isFile() && entry.name.endsWith(".html") ? [full] : [];
- })
- );
- return files.flat();
-}
-
-async function injectBridge() {
- const files = await findHtmlFiles(dest);
- await Promise.all(
- files.map(async (fullPath) => {
- const html = await fs.readFile(fullPath, "utf8");
- if (html.includes("tauri-bridge.js")) return;
-
- const scriptPath = path
- .relative(path.dirname(fullPath), path.join(dest, "tauri-bridge.js"))
- .replace(/\\/g, "/");
- const injection = `\n \n