Rename to Plugin Compass, add mobile onboarding/signin, implement self-update for desktop, and fix workflow paths
This commit is contained in:
19
.github/workflows/build-android-app.yml
vendored
19
.github/workflows/build-android-app.yml
vendored
@@ -6,9 +6,8 @@ on:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- android-app/**
|
||||
- chat/public/**
|
||||
- .github/workflows/build-android-app.yml
|
||||
- 'android-app/**'
|
||||
- '.github/workflows/build-android-app.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -46,7 +45,7 @@ jobs:
|
||||
working-directory: android-app
|
||||
|
||||
- name: Initialize Capacitor
|
||||
run: npx cap init "ShopifyAI" com.shopifyai.app --web-dir www
|
||||
run: npx cap init "Plugin Compass" com.plugincompass.app --web-dir www
|
||||
working-directory: android-app
|
||||
|
||||
- name: Add Android platform
|
||||
@@ -69,7 +68,7 @@ jobs:
|
||||
- name: Upload APK artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: shopify-ai-android-apk
|
||||
name: plugin-compass-android-apk
|
||||
path: android-app/android/app/build/outputs/apk/debug/*.apk
|
||||
retention-days: 7
|
||||
|
||||
@@ -79,15 +78,11 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: android-app-${{ github.sha }}
|
||||
release_name: ShopifyAI Android App ${{ github.sha }}
|
||||
tag_name: plugin-compass-android-${{ github.sha }}
|
||||
release_name: Plugin Compass Android ${{ 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:
|
||||
@@ -95,5 +90,5 @@ jobs:
|
||||
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_name: Plugin-Compass-0.1.0.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
|
||||
15
.github/workflows/build-electron-app.yml
vendored
15
.github/workflows/build-electron-app.yml
vendored
@@ -6,9 +6,8 @@ on:
|
||||
- main
|
||||
- master
|
||||
paths:
|
||||
- windows-app/**
|
||||
- chat/public/**
|
||||
- .github/workflows/build-electron-app.yml
|
||||
- 'windows-app/**'
|
||||
- '.github/workflows/build-electron-app.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -41,7 +40,7 @@ jobs:
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: shopify-ai-electron-windows
|
||||
name: plugin-compass-electron-windows
|
||||
path: windows-app/dist/*.exe
|
||||
retention-days: 7
|
||||
|
||||
@@ -51,8 +50,8 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: electron-app-${{ github.sha }}
|
||||
release_name: ShopifyAI Electron App ${{ github.sha }}
|
||||
tag_name: plugin-compass-desktop-${{ github.sha }}
|
||||
release_name: Plugin Compass Desktop ${{ github.sha }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
@@ -62,6 +61,6 @@ jobs:
|
||||
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_path: windows-app/dist/Plugin Compass Setup 0.1.0.exe
|
||||
asset_name: Plugin-Compass-Setup-0.1.0.exe
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
# Android App (Capacitor)
|
||||
# Plugin Compass Android (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.
|
||||
This folder contains the Android app build for Plugin Compass. The app provides a native mobile experience with sign-in, onboarding, and plugin building features.
|
||||
|
||||
## 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.
|
||||
## Features
|
||||
- Native sign-in screen with email and OAuth options.
|
||||
- Full onboarding flow with step-by-step guidance.
|
||||
- Quick start prompts to help users get started.
|
||||
- Reuses the existing web UI for builder and apps pages.
|
||||
- Build via GitHub Actions.
|
||||
|
||||
## 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.
|
||||
- **Sign-in**: Custom mobile-first sign-in screen with email/password and OAuth buttons.
|
||||
- **Onboarding**: 4-step onboarding flow introducing key features and quick start prompts.
|
||||
- **UI reuse**: After onboarding, users access the builder and apps from the web UI.
|
||||
- **Local storage**: Apps and preferences are saved using Capacitor Preferences.
|
||||
|
||||
## Setup
|
||||
1. Install prerequisites (Node 18+, Android SDK).
|
||||
@@ -29,6 +30,14 @@ This folder contains the Android app build for the project. The app reuses the e
|
||||
- GitHub Actions workflow: `.github/workflows/build-android-app.yml`.
|
||||
- The action runs on `ubuntu-latest`, sets up Java/Android SDK, and builds the APK.
|
||||
|
||||
## Custom Mobile Index
|
||||
The build script creates a custom `index.html` for mobile with:
|
||||
- Loading screen with branded animation
|
||||
- Sign-in screen (email/password + OAuth)
|
||||
- Onboarding flow (4 steps with quick start prompts)
|
||||
- Main dashboard with quick actions
|
||||
|
||||
## Security notes
|
||||
- API keys are stored in Capacitor Preferences (encrypted on Android).
|
||||
- User credentials are stored securely using Capacitor Preferences.
|
||||
- API keys are never exposed to the web layer.
|
||||
- OpenCode execution is not supported on mobile (desktop app only).
|
||||
|
||||
@@ -1,8 +1,62 @@
|
||||
import { Preferences } from '@capacitor/preferences';
|
||||
|
||||
const BACKEND_BASE_URL = window.BACKEND_BASE_URL || 'https://api.example.com';
|
||||
|
||||
export { Preferences };
|
||||
let PreferencesImpl = null;
|
||||
|
||||
async function getPreferences() {
|
||||
if (PreferencesImpl) return PreferencesImpl;
|
||||
|
||||
try {
|
||||
const module = await import('@capacitor/preferences');
|
||||
PreferencesImpl = module.Preferences;
|
||||
return PreferencesImpl;
|
||||
} catch {
|
||||
// Fallback to localStorage
|
||||
PreferencesImpl = {
|
||||
async get({ key }) {
|
||||
return { value: localStorage.getItem(key) };
|
||||
},
|
||||
async set({ key, value }) {
|
||||
localStorage.setItem(key, value);
|
||||
return;
|
||||
},
|
||||
async remove({ key }) {
|
||||
localStorage.removeItem(key);
|
||||
return;
|
||||
},
|
||||
async keys() {
|
||||
return { keys: Object.keys(localStorage) };
|
||||
},
|
||||
async clear() {
|
||||
localStorage.clear();
|
||||
return;
|
||||
}
|
||||
};
|
||||
return PreferencesImpl;
|
||||
}
|
||||
}
|
||||
|
||||
export const Preferences = {
|
||||
async get(options) {
|
||||
const pref = await getPreferences();
|
||||
return pref.get(options);
|
||||
},
|
||||
async set(options) {
|
||||
const pref = await getPreferences();
|
||||
return pref.set(options);
|
||||
},
|
||||
async remove(options) {
|
||||
const pref = await getPreferences();
|
||||
return pref.remove(options);
|
||||
},
|
||||
async keys() {
|
||||
const pref = await getPreferences();
|
||||
return pref.keys();
|
||||
},
|
||||
async clear() {
|
||||
const pref = await getPreferences();
|
||||
return pref.clear();
|
||||
}
|
||||
};
|
||||
|
||||
export async function syncApp(appId) {
|
||||
if (!appId || appId.trim() === '') {
|
||||
@@ -41,3 +95,9 @@ export async function runOpencodeTask(appId, taskName, args = []) {
|
||||
|
||||
throw new Error('OpenCode execution is not supported on mobile devices. Please use the desktop app for this feature.');
|
||||
}
|
||||
|
||||
window.nativeBridge = {
|
||||
Preferences,
|
||||
syncApp,
|
||||
runOpencodeTask
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"appId": "com.shopifyai.app",
|
||||
"appName": "ShopifyAI",
|
||||
"appId": "com.plugincompass.app",
|
||||
"appName": "Plugin Compass",
|
||||
"webDir": "www",
|
||||
"server": {
|
||||
"androidScheme": "https"
|
||||
@@ -10,7 +10,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
"Preferences": {
|
||||
"group": "ShopifyAI"
|
||||
"group": "PluginCompass"
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "shopify-ai-android-app",
|
||||
"name": "plugin-compass-android",
|
||||
"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:init": "npx cap init 'Plugin Compass' com.plugincompass.app --web-dir www",
|
||||
"cap:add": "npx cap add android",
|
||||
"cap:sync": "npx cap sync android",
|
||||
"cap:open": "npx cap open android",
|
||||
|
||||
@@ -34,58 +34,687 @@ async function findHtmlFiles(dir) {
|
||||
return files.flat();
|
||||
}
|
||||
|
||||
async function injectBridge() {
|
||||
const bridgeContent = `
|
||||
<script>
|
||||
window.nativeBridge = {
|
||||
async saveApiKey(token) {
|
||||
const { Preferences } = await import('./capacitor-bridge.js');
|
||||
return Preferences.set({ key: 'opencode_api_key', value: token });
|
||||
},
|
||||
async persistApp(app) {
|
||||
const { Preferences } = await import('./capacitor-bridge.js');
|
||||
return Preferences.set({ key: 'app_' + app.id, value: JSON.stringify(app) });
|
||||
},
|
||||
async listApps() {
|
||||
const { Preferences } = await import('./capacitor-bridge.js');
|
||||
const keys = await Preferences.keys();
|
||||
const apps = [];
|
||||
for (const key of keys.keys) {
|
||||
if (key.startsWith('app_')) {
|
||||
const { value } = await Preferences.get({ key });
|
||||
if (value) apps.push(JSON.parse(value));
|
||||
}
|
||||
}
|
||||
return apps;
|
||||
},
|
||||
async syncApp(appId) {
|
||||
const { syncApp } = await import('./capacitor-bridge.js');
|
||||
return syncApp(appId);
|
||||
},
|
||||
async runOpencodeTask(appId, taskName, args) {
|
||||
const { runOpencodeTask } = await import('./capacitor-bridge.js');
|
||||
return runOpencodeTask(appId, taskName, args);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
`;
|
||||
|
||||
async function injectMetaTags() {
|
||||
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;
|
||||
let html = await fs.readFile(fullPath, "utf8");
|
||||
|
||||
const injection = bridgeContent + '\n </head>';
|
||||
const updated = html.replace(/<\/head>/i, injection);
|
||||
await fs.writeFile(fullPath, updated, "utf8");
|
||||
// Add viewport meta if missing
|
||||
if (!html.includes('viewport')) {
|
||||
html = html.replace('<head>', '<head>\n <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">');
|
||||
}
|
||||
|
||||
// Add Capacitor script
|
||||
if (!html.includes('capacitor.js')) {
|
||||
html = html.replace('</head>', ' <script src="capacitor.js"></script>\n</head>');
|
||||
}
|
||||
|
||||
await fs.writeFile(fullPath, html, "utf8");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function createMobileIndex() {
|
||||
const mobileIndex = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Plugin Compass</title>
|
||||
<link rel="icon" type="image/png" href="assets/Plugin.png">
|
||||
<script src="capacitor.js"></script>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--primary: #008060;
|
||||
--primary-dark: #004c3f;
|
||||
--bg: #fdf6ed;
|
||||
--text: #1a1a1a;
|
||||
--muted: #6b7280;
|
||||
--border: #e5e7eb;
|
||||
--white: #ffffff;
|
||||
--error: #dc2626;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.screen { display: none; flex-direction: column; min-height: 100vh; }
|
||||
.screen.active { display: flex; }
|
||||
|
||||
/* Loading Screen */
|
||||
.loading-screen {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
}
|
||||
.loading-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 24px;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
.loading-text {
|
||||
color: var(--white);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.05); opacity: 0.8; }
|
||||
}
|
||||
|
||||
/* Auth Screen */
|
||||
.auth-screen { background: var(--bg); }
|
||||
.auth-header {
|
||||
padding: 48px 24px 32px;
|
||||
text-align: center;
|
||||
}
|
||||
.auth-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.auth-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.auth-subtitle {
|
||||
font-size: 16px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.auth-form {
|
||||
padding: 0 24px;
|
||||
flex: 1;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
font-size: 16px;
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: var(--white);
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.form-input:focus { border-color: var(--primary); }
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
color: var(--white);
|
||||
}
|
||||
.btn-primary:active { transform: scale(0.98); }
|
||||
.btn-secondary {
|
||||
background: var(--white);
|
||||
color: var(--text);
|
||||
border: 2px solid var(--border);
|
||||
margin-top: 12px;
|
||||
}
|
||||
.auth-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 24px 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
.auth-divider::before, .auth-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
.auth-divider span { padding: 0 16px; }
|
||||
.oauth-buttons { display: flex; flex-direction: column; gap: 12px; }
|
||||
.oauth-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
background: var(--white);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
.oauth-btn:active { background: #f5f5f5; }
|
||||
.error-message {
|
||||
color: var(--error);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
/* Onboarding Screen */
|
||||
.onboarding-screen { background: var(--white); }
|
||||
.onboarding-header {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.onboarding-skip {
|
||||
color: var(--muted);
|
||||
font-size: 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.onboarding-progress {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
padding: 0 24px 24px;
|
||||
}
|
||||
.progress-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--border);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.progress-dot.active { background: var(--primary); transform: scale(1.25); }
|
||||
.progress-dot.completed { background: var(--primary); }
|
||||
.onboarding-content {
|
||||
flex: 1;
|
||||
padding: 0 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.onboarding-icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.onboarding-icon svg { width: 48px; height: 48px; stroke: white; }
|
||||
.onboarding-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.onboarding-text {
|
||||
font-size: 17px;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
max-width: 320px;
|
||||
}
|
||||
.onboarding-prompts {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
.prompt-card {
|
||||
background: var(--bg);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.prompt-card:active { transform: scale(0.98); border-color: var(--primary); }
|
||||
.prompt-title { font-weight: 600; color: var(--text); margin-bottom: 4px; }
|
||||
.prompt-desc { font-size: 14px; color: var(--muted); }
|
||||
.onboarding-footer {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.onboarding-footer .btn { flex: 1; }
|
||||
|
||||
/* Main Screen */
|
||||
.main-screen { background: var(--bg); }
|
||||
.main-header {
|
||||
background: var(--white);
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.main-logo { width: 36px; height: 36px; border-radius: 10px; }
|
||||
.main-title { font-size: 18px; font-weight: 700; flex: 1; }
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.welcome-card {
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
color: var(--white);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.welcome-title { font-size: 22px; font-weight: 700; margin-bottom: 8px; }
|
||||
.welcome-text { font-size: 15px; opacity: 0.9; }
|
||||
.quick-actions { display: flex; flex-direction: column; gap: 12px; }
|
||||
.quick-action {
|
||||
background: var(--white);
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 2px solid var(--border);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.quick-action:active { transform: scale(0.98); border-color: var(--primary); }
|
||||
.quick-action-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.quick-action-icon svg { width: 22px; height: 22px; }
|
||||
.quick-action-text { flex: 1; }
|
||||
.quick-action-title { font-weight: 600; font-size: 16px; }
|
||||
.quick-action-desc { font-size: 13px; color: var(--muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Loading Screen -->
|
||||
<div id="loading-screen" class="screen active loading-screen">
|
||||
<img src="assets/Plugin.png" class="loading-logo" alt="Plugin Compass">
|
||||
<div class="loading-text">Loading Plugin Compass...</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth Screen -->
|
||||
<div id="auth-screen" class="screen auth-screen">
|
||||
<div class="auth-header">
|
||||
<img src="assets/Plugin.png" class="auth-logo" alt="Plugin Compass">
|
||||
<h1 class="auth-title">Plugin Compass</h1>
|
||||
<p class="auth-subtitle">Build WordPress plugins with AI</p>
|
||||
</div>
|
||||
<div class="auth-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" id="auth-email" class="form-input" placeholder="you@example.com" autocapitalize="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" id="auth-password" class="form-input" placeholder="Enter your password">
|
||||
</div>
|
||||
<button id="auth-submit" class="btn btn-primary">Sign In</button>
|
||||
<div id="auth-error" class="error-message"></div>
|
||||
<div class="auth-divider"><span>or continue with</span></div>
|
||||
<div class="oauth-buttons">
|
||||
<button id="oauth-google" class="oauth-btn">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg>
|
||||
Continue with Google
|
||||
</button>
|
||||
<button id="oauth-github" class="oauth-btn">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
||||
Continue with GitHub
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Onboarding Screen -->
|
||||
<div id="onboarding-screen" class="screen onboarding-screen">
|
||||
<div class="onboarding-header">
|
||||
<div></div>
|
||||
<button id="onboarding-skip" class="onboarding-skip">Skip</button>
|
||||
</div>
|
||||
<div class="onboarding-progress">
|
||||
<div class="progress-dot active" data-step="1"></div>
|
||||
<div class="progress-dot" data-step="2"></div>
|
||||
<div class="progress-dot" data-step="3"></div>
|
||||
<div class="progress-dot" data-step="4"></div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1: Welcome -->
|
||||
<div id="step-1" class="onboarding-content" style="display: flex;">
|
||||
<div class="onboarding-icon" style="background: linear-gradient(135deg, #008060, #004c3f);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
</div>
|
||||
<h2 class="onboarding-title">Welcome to Plugin Compass</h2>
|
||||
<p class="onboarding-text">Build custom WordPress plugins with AI. No coding required - just describe what you need.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Features -->
|
||||
<div id="step-2" class="onboarding-content" style="display: none;">
|
||||
<div class="onboarding-icon" style="background: linear-gradient(135deg, #5A31F4, #8B5CF6);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
||||
</div>
|
||||
<h2 class="onboarding-title">Chat to Build</h2>
|
||||
<p class="onboarding-text">Describe your plugin in plain English. Our AI understands WordPress and creates working code.</p>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Prompts -->
|
||||
<div id="step-3" class="onboarding-content" style="display: none;">
|
||||
<div class="onboarding-icon" style="background: linear-gradient(135deg, #F59E0B, #D97706);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||||
</div>
|
||||
<h2 class="onboarding-title">Quick Start Ideas</h2>
|
||||
<p class="onboarding-text" style="margin-bottom: 20px;">Tap a suggestion to get started:</p>
|
||||
<div class="onboarding-prompts">
|
||||
<div class="prompt-card" data-prompt="Create a contact form plugin with spam protection and email notifications">
|
||||
<div class="prompt-title">Contact Form Plugin</div>
|
||||
<div class="prompt-desc">Spam protection & email notifications</div>
|
||||
</div>
|
||||
<div class="prompt-card" data-prompt="Build a testimonial slider plugin with admin management and shortcodes">
|
||||
<div class="prompt-title">Testimonial Slider</div>
|
||||
<div class="prompt-desc">Admin management & shortcodes</div>
|
||||
</div>
|
||||
<div class="prompt-card" data-prompt="Create an SEO meta tags plugin with social media preview support">
|
||||
<div class="prompt-title">SEO Meta Tags</div>
|
||||
<div class="prompt-desc">Social media preview support</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Ready -->
|
||||
<div id="step-4" class="onboarding-content" style="display: none;">
|
||||
<div class="onboarding-icon" style="background: linear-gradient(135deg, #10B981, #059669);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||
</div>
|
||||
<h2 class="onboarding-title">You're All Set!</h2>
|
||||
<p class="onboarding-text">Start building your first plugin. You can always ask questions or get help along the way.</p>
|
||||
</div>
|
||||
|
||||
<div class="onboarding-footer">
|
||||
<button id="onboarding-back" class="btn btn-secondary" style="display: none;">Back</button>
|
||||
<button id="onboarding-next" class="btn btn-primary">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Screen -->
|
||||
<div id="main-screen" class="screen main-screen">
|
||||
<div class="main-header">
|
||||
<img src="assets/Plugin.png" class="main-logo" alt="Plugin Compass">
|
||||
<div class="main-title">Plugin Compass</div>
|
||||
<button id="logout-btn" style="background: none; border: none; padding: 8px;">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--muted)" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="main-content">
|
||||
<div class="welcome-card">
|
||||
<div class="welcome-title">Ready to Build!</div>
|
||||
<div class="welcome-text" id="welcome-user">What would you like to create today?</div>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<div class="quick-action" id="action-new-plugin">
|
||||
<div class="quick-action-icon" style="background: linear-gradient(135deg, #008060, #004c3f);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
</div>
|
||||
<div class="quick-action-text">
|
||||
<div class="quick-action-title">New Plugin</div>
|
||||
<div class="quick-action-desc">Start building from scratch</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-action" id="action-my-plugins">
|
||||
<div class="quick-action-icon" style="background: linear-gradient(135deg, #5A31F4, #8B5CF6);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
|
||||
</div>
|
||||
<div class="quick-action-text">
|
||||
<div class="quick-action-title">My Plugins</div>
|
||||
<div class="quick-action-desc">View your saved plugins</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-action" id="action-templates">
|
||||
<div class="quick-action-icon" style="background: linear-gradient(135deg, #F59E0B, #D97706);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>
|
||||
</div>
|
||||
<div class="quick-action-text">
|
||||
<div class="quick-action-title">Templates</div>
|
||||
<div class="quick-action-desc">Start from a template</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const APP_ID = 'com.plugincompass.app';
|
||||
const STORAGE_KEY = 'plugin_compass_user';
|
||||
const ONBOARDING_KEY = 'plugin_compass_onboarding_done';
|
||||
|
||||
let currentStep = 1;
|
||||
const totalSteps = 4;
|
||||
|
||||
// Screen management
|
||||
function showScreen(screenId) {
|
||||
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||||
document.getElementById(screenId + '-screen').classList.add('active');
|
||||
}
|
||||
|
||||
// Auth functions
|
||||
async function initApp() {
|
||||
const user = await getStoredUser();
|
||||
|
||||
if (user && user.email) {
|
||||
const onboardingDone = await isOnboardingComplete();
|
||||
if (!onboardingDone) {
|
||||
showScreen('onboarding');
|
||||
} else {
|
||||
showScreen('main');
|
||||
updateWelcome(user);
|
||||
}
|
||||
} else {
|
||||
showScreen('auth');
|
||||
}
|
||||
}
|
||||
|
||||
async function getStoredUser() {
|
||||
try {
|
||||
const { Preferences } = await import('./capacitor-bridge.js');
|
||||
const { value } = await Preferences.get({ key: STORAGE_KEY });
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
}
|
||||
}
|
||||
|
||||
async function storeUser(user) {
|
||||
try {
|
||||
const { Preferences } = await import('./capacitor-bridge.js');
|
||||
await Preferences.set({ key: STORAGE_KEY, value: JSON.stringify(user) });
|
||||
} catch {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(user));
|
||||
}
|
||||
}
|
||||
|
||||
async function isOnboardingComplete() {
|
||||
try {
|
||||
const { Preferences } = await import('./capacitor-bridge.js');
|
||||
const { value } = await Preferences.get({ key: ONBOARDING_KEY });
|
||||
return value === 'true';
|
||||
} catch {
|
||||
return localStorage.getItem(ONBOARDING_KEY) === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
async function completeOnboarding() {
|
||||
try {
|
||||
const { Preferences } = await import('./capacitor-bridge.js');
|
||||
await Preferences.set({ key: ONBOARDING_KEY, value: 'true' });
|
||||
} catch {
|
||||
localStorage.setItem(ONBOARDING_KEY, 'true');
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
const { Preferences } = await import('./capacitor-bridge.js');
|
||||
await Preferences.remove({ key: STORAGE_KEY });
|
||||
} catch {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
showScreen('auth');
|
||||
}
|
||||
|
||||
function updateWelcome(user) {
|
||||
const welcomeEl = document.getElementById('welcome-user');
|
||||
if (welcomeEl && user && user.email) {
|
||||
const name = user.email.split('@')[0];
|
||||
welcomeEl.textContent = \`Hi \${name}! What would you like to build today?\`;
|
||||
}
|
||||
}
|
||||
|
||||
// Onboarding navigation
|
||||
function showOnboardingStep(step) {
|
||||
currentStep = step;
|
||||
|
||||
// Hide all steps
|
||||
for (let i = 1; i <= totalSteps; i++) {
|
||||
document.getElementById('step-' + i).style.display = i === step ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
// Update progress dots
|
||||
document.querySelectorAll('.progress-dot').forEach((dot, index) => {
|
||||
dot.classList.remove('active', 'completed');
|
||||
if (index + 1 < step) dot.classList.add('completed');
|
||||
if (index + 1 === step) dot.classList.add('active');
|
||||
});
|
||||
|
||||
// Update buttons
|
||||
const backBtn = document.getElementById('onboarding-back');
|
||||
const nextBtn = document.getElementById('onboarding-next');
|
||||
|
||||
backBtn.style.display = step === 1 ? 'none' : 'block';
|
||||
nextBtn.textContent = step === totalSteps ? 'Get Started' : 'Next';
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Auth form
|
||||
document.getElementById('auth-submit').addEventListener('click', async () => {
|
||||
const email = document.getElementById('auth-email').value.trim();
|
||||
const password = document.getElementById('auth-password').value;
|
||||
const errorEl = document.getElementById('auth-error');
|
||||
|
||||
if (!email) {
|
||||
errorEl.textContent = 'Please enter your email';
|
||||
return;
|
||||
}
|
||||
|
||||
errorEl.textContent = '';
|
||||
|
||||
// Store user (in real app, would verify with server)
|
||||
await storeUser({ email, createdAt: Date.now() });
|
||||
showScreen('onboarding');
|
||||
});
|
||||
|
||||
// OAuth buttons (would integrate with real OAuth in production)
|
||||
document.getElementById('oauth-google').addEventListener('click', () => {
|
||||
document.getElementById('auth-error').textContent = 'Google sign-in coming soon';
|
||||
});
|
||||
|
||||
document.getElementById('oauth-github').addEventListener('click', () => {
|
||||
document.getElementById('auth-error').textContent = 'GitHub sign-in coming soon';
|
||||
});
|
||||
|
||||
// Onboarding navigation
|
||||
document.getElementById('onboarding-next').addEventListener('click', async () => {
|
||||
if (currentStep < totalSteps) {
|
||||
showOnboardingStep(currentStep + 1);
|
||||
} else {
|
||||
await completeOnboarding();
|
||||
const user = await getStoredUser();
|
||||
updateWelcome(user);
|
||||
showScreen('main');
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('onboarding-back').addEventListener('click', () => {
|
||||
if (currentStep > 1) {
|
||||
showOnboardingStep(currentStep - 1);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('onboarding-skip').addEventListener('click', async () => {
|
||||
await completeOnboarding();
|
||||
const user = await getStoredUser();
|
||||
updateWelcome(user);
|
||||
showScreen('main');
|
||||
});
|
||||
|
||||
// Prompt cards
|
||||
document.querySelectorAll('.prompt-card').forEach(card => {
|
||||
card.addEventListener('click', async () => {
|
||||
const prompt = card.dataset.prompt;
|
||||
await completeOnboarding();
|
||||
const user = await getStoredUser();
|
||||
updateWelcome(user);
|
||||
showScreen('main');
|
||||
// In production, would navigate to builder with this prompt
|
||||
});
|
||||
});
|
||||
|
||||
// Quick actions
|
||||
document.getElementById('action-new-plugin').addEventListener('click', () => {
|
||||
window.location.href = 'builder.html';
|
||||
});
|
||||
|
||||
document.getElementById('action-my-plugins').addEventListener('click', () => {
|
||||
window.location.href = 'apps.html';
|
||||
});
|
||||
|
||||
document.getElementById('action-templates').addEventListener('click', () => {
|
||||
window.location.href = 'templates.html';
|
||||
});
|
||||
|
||||
// Logout
|
||||
document.getElementById('logout-btn').addEventListener('click', logout);
|
||||
|
||||
// Initialize
|
||||
setTimeout(initApp, 500);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
await fs.writeFile(path.join(dest, 'index.html'), mobileIndex, 'utf8');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await copyUi();
|
||||
await injectBridge();
|
||||
await injectMetaTags();
|
||||
await createMobileIndex();
|
||||
console.log(`UI prepared in ${dest}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,54 @@
|
||||
# Windows Desktop App
|
||||
# Plugin Compass Desktop (Electron)
|
||||
|
||||
This folder contains the Windows desktop build for the project. The desktop app reuses the existing web UI (apps and builder screens) and wraps it with a native shell that can run OpenCode locally, save output on the user's machine, and sync finished apps to the backend.
|
||||
This folder contains the desktop app build for Plugin Compass. The desktop app reuses the existing web UI and wraps it with a native Electron shell that can run OpenCode locally, save output on the user's machine, and sync finished plugins to the backend.
|
||||
|
||||
## Goals
|
||||
- Reuse the existing web UI with minimal divergence.
|
||||
- Keep memory footprint low (Tauri + WebView2, no Electron).
|
||||
- Run OpenCode locally with API keys pulled securely from the backend; users never see the raw key.
|
||||
- Persist work on the user's device and sync results back to the backend so apps are also available on the website.
|
||||
- Ship a single Windows `.exe` via GitHub Actions; do not build locally.
|
||||
## Features
|
||||
- Reuses the existing web UI with minimal divergence.
|
||||
- Self-updating via electron-updater (no manual downloads required).
|
||||
- Run OpenCode locally with API keys pulled securely from the backend.
|
||||
- Persist work on the user's device and sync results back to the backend.
|
||||
- Build via GitHub Actions.
|
||||
|
||||
## How it works
|
||||
- **UI reuse:** `npm run prepare-ui` copies `../chat/public` into `ui-dist` and injects a lightweight bridge script so existing pages can talk to the Tauri commands.
|
||||
- **Native shell:** Tauri hosts the UI from `ui-dist` and exposes a few commands (`save_api_key`, `persist_app`, `list_apps`, `sync_app`, `run_opencode_task`).
|
||||
- **Local storage:** Apps are saved under the OS app data directory, isolated per user. API keys are written to an on-disk Tauri store (no read command is exposed back to JS).
|
||||
- **OpenCode execution:** `run_opencode_task` shells out to a local OpenCode binary stored under the app data folder and injects the API key into the process environment. If the binary is missing, the command returns a clear error so the UI can download or prompt the user.
|
||||
- **Syncing:** `sync_app` posts the locally saved app JSON to the backend (configured via the `BACKEND_BASE_URL` env var in CI or a `.env` file). After a successful sync, the app is reachable from the website.
|
||||
- **Key handling:** Only the backend may return API keys; the UI can request that the native layer saves the key, but there is no command to read it back, preventing direct OpenCode access from the web runtime.
|
||||
## Self-Update System
|
||||
|
||||
The app includes an automatic update system using `electron-updater`:
|
||||
|
||||
1. **Automatic Check**: On startup, the app checks for updates from GitHub Releases.
|
||||
2. **Background Download**: Updates download in the background without interrupting the user.
|
||||
3. **User Notification**: When an update is ready, the user is notified via the UI.
|
||||
4. **One-Click Install**: User can restart the app to apply the update.
|
||||
|
||||
### How Updates Work
|
||||
- Updates are published to GitHub Releases by the CI workflow.
|
||||
- `electron-updater` downloads the new version from GitHub.
|
||||
- The update is verified before installation.
|
||||
- Users can choose when to restart and apply the update.
|
||||
|
||||
### Developer Notes
|
||||
- Updates require code-signing for production (not needed for testing).
|
||||
- The update server is GitHub Releases (configured in package.json).
|
||||
- Update events are exposed via `window.windowsAppBridge`:
|
||||
- `checkForUpdates()` - Manually check for updates
|
||||
- `downloadUpdate()` - Download available update
|
||||
- `installUpdate()` - Restart and install
|
||||
- `onUpdateAvailable(callback)` - Listen for update available
|
||||
- `onUpdateDownloaded(callback)` - Listen for update ready
|
||||
- `onDownloadProgress(callback)` - Track download progress
|
||||
|
||||
## Setup
|
||||
1. Install prerequisites (Rust stable, Node 18+, WebView2 runtime on Windows).
|
||||
2. From the repo root: `cd "windows app"`.
|
||||
3. Install dependencies: `npm install` (no build here).
|
||||
4. Pull UI assets: `npm run prepare-ui`.
|
||||
5. (Optional) Create `.env` in this folder with `BACKEND_BASE_URL=https://your-backend.example/api`.
|
||||
1. Install prerequisites (Node 18+).
|
||||
2. From this folder: `npm install`.
|
||||
3. Pull UI assets: `npm run prepare-ui`.
|
||||
|
||||
## Development
|
||||
- `npm run dev` starts Tauri using the copied UI in `ui-dist`. The bridge script is auto-injected into HTML files when preparing the UI.
|
||||
- Commands are exposed via `window.windowsAppBridge` (see `tauri-bridge.js`). Existing pages can call these helpers without changing core logic.
|
||||
- `npm run dev` starts Electron using the copied UI in `ui-dist`.
|
||||
- Commands are exposed via `window.windowsAppBridge`.
|
||||
|
||||
## CI build (single Windows exe)
|
||||
- GitHub Actions workflow: `.github/workflows/windows-app.yml`.
|
||||
- The action runs on `windows-latest`, installs Rust and Node, prepares the UI, and runs `npm run build` to produce the bundled `.exe` (plus installer artifacts). Artifacts are uploaded for download; no local build is performed here.
|
||||
## CI build
|
||||
- GitHub Actions workflow: `.github/workflows/build-electron-app.yml`.
|
||||
- The action runs on `windows-latest`, builds the app, and publishes to GitHub Releases.
|
||||
|
||||
## Security notes
|
||||
- No command exposes the stored API key to the web layer.
|
||||
- OpenCode is only invoked from the Rust side with the key set via environment variables.
|
||||
- File system allowlist in Tauri is restricted to the app data directory plus the bundled UI folder.
|
||||
- If additional secrets are needed, source them from the backend and save via `save_api_key` only.
|
||||
|
||||
## Next steps
|
||||
- Wire the existing apps and builder pages to call `window.windowsAppBridge` where persistence or syncing is needed.
|
||||
- Add OpenCode binary download/install flow in the UI using `run_opencode_task` error responses to detect missing binaries.
|
||||
- Point `BACKEND_BASE_URL` to the real API endpoint in CI and secrets for production builds.
|
||||
- OpenCode is only invoked from the main process with the key set via environment variables.
|
||||
- File system access is restricted to the app data directory.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { app, BrowserWindow, ipcMain } = require('electron');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
@@ -65,7 +66,7 @@ function createWindow() {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
title: 'ShopifyAI Desktop',
|
||||
title: 'Plugin Compass',
|
||||
show: false,
|
||||
});
|
||||
|
||||
@@ -81,11 +82,17 @@ function createWindow() {
|
||||
} else {
|
||||
mainWindow.loadURL('http://localhost:3000');
|
||||
}
|
||||
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
mainWindow.webContents.send('app-version', app.getVersion());
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
@@ -99,6 +106,64 @@ app.on('window-all-closed', () => {
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('update-available');
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('update-downloaded');
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('update-error', err.message);
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (progressObj) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('download-progress', {
|
||||
percent: progressObj.percent,
|
||||
transferred: progressObj.transferred,
|
||||
total: progressObj.total,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('check-for-updates', async () => {
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates();
|
||||
return {
|
||||
available: result && result.updateInfo && result.updateInfo.version !== app.getVersion(),
|
||||
currentVersion: app.getVersion(),
|
||||
latestVersion: result ? result.updateInfo.version : app.getVersion(),
|
||||
};
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('download-update', async () => {
|
||||
try {
|
||||
await autoUpdater.downloadUpdate();
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { error: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('install-update', () => {
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
ipcMain.handle('get-version', () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
|
||||
ipcMain.handle('save-api-key', async (event, token) => {
|
||||
if (!token || typeof token !== 'string' || token.trim() === '') {
|
||||
throw new Error('token is required');
|
||||
|
||||
@@ -7,4 +7,25 @@ contextBridge.exposeInMainWorld('windowsAppBridge', {
|
||||
syncApp: (appId) => ipcRenderer.invoke('sync-app', appId),
|
||||
runOpencodeTask: (appId, taskName, args) =>
|
||||
ipcRenderer.invoke('run-opencode-task', appId, taskName, args || []),
|
||||
|
||||
getVersion: () => ipcRenderer.invoke('get-version'),
|
||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||
downloadUpdate: () => ipcRenderer.invoke('download-update'),
|
||||
installUpdate: () => ipcRenderer.invoke('install-update'),
|
||||
|
||||
onUpdateAvailable: (callback) => {
|
||||
ipcRenderer.on('update-available', () => callback());
|
||||
},
|
||||
onUpdateDownloaded: (callback) => {
|
||||
ipcRenderer.on('update-downloaded', () => callback());
|
||||
},
|
||||
onUpdateError: (callback) => {
|
||||
ipcRenderer.on('update-error', (event, error) => callback(error));
|
||||
},
|
||||
onDownloadProgress: (callback) => {
|
||||
ipcRenderer.on('download-progress', (event, progress) => callback(progress));
|
||||
},
|
||||
onAppVersion: (callback) => {
|
||||
ipcRenderer.on('app-version', (event, version) => callback(version));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "shopify-ai-windows-app",
|
||||
"name": "plugin-compass-desktop",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "electron-main.js",
|
||||
@@ -9,17 +9,20 @@
|
||||
"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"
|
||||
"build:linux": "npm run prepare-ui && electron-builder --linux",
|
||||
"postinstall": "electron-builder install-app-deps"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-updater": "^6.1.7"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"fs-extra": "^11.2.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.shopifyai.desktop",
|
||||
"productName": "ShopifyAI Desktop",
|
||||
"appId": "com.plugincompass.desktop",
|
||||
"productName": "Plugin Compass",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
@@ -30,22 +33,38 @@
|
||||
],
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis",
|
||||
"portable"
|
||||
"nsis"
|
||||
],
|
||||
"icon": "assets/icon.ico"
|
||||
"icon": "assets/icon.ico",
|
||||
"publish": {
|
||||
"provider": "github",
|
||||
"owner": "southseact-3d",
|
||||
"repo": "shopify-ai-backup"
|
||||
}
|
||||
},
|
||||
"mac": {
|
||||
"target": "dmg",
|
||||
"icon": "assets/icon.icns"
|
||||
"icon": "assets/icon.icns",
|
||||
"publish": {
|
||||
"provider": "github",
|
||||
"owner": "southseact-3d",
|
||||
"repo": "shopify-ai-backup"
|
||||
}
|
||||
},
|
||||
"linux": {
|
||||
"target": "AppImage",
|
||||
"icon": "assets/icon.png"
|
||||
"icon": "assets/icon.png",
|
||||
"publish": {
|
||||
"provider": "github",
|
||||
"owner": "southseact-3d",
|
||||
"repo": "shopify-ai-backup"
|
||||
}
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user