Rename to Plugin Compass, add mobile onboarding/signin, implement self-update for desktop, and fix workflow paths

This commit is contained in:
southseact-3d
2026-02-16 12:05:30 +00:00
parent 63698e1d19
commit d6e2af3a29
11 changed files with 945 additions and 138 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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).

View File

@@ -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
};

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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}`);
}

View File

@@ -1,44 +1,54 @@
# Windows Desktop App
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.
## 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.
## 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.
## 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`.
## 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.
## 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.
## 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.
# Plugin Compass Desktop (Electron)
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.
## 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.
## 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 (Node 18+).
2. From this folder: `npm install`.
3. Pull UI assets: `npm run prepare-ui`.
## Development
- `npm run dev` starts Electron using the copied UI in `ui-dist`.
- Commands are exposed via `window.windowsAppBridge`.
## 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 main process with the key set via environment variables.
- File system access is restricted to the app data directory.

View File

@@ -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,10 +82,16 @@ 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) {
@@ -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');

View File

@@ -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));
},
});

View File

@@ -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
}
}
}