Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/workflows/desktop-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,107 @@ jobs:
frontend/src-tauri/target/release/bundle/appimage/*.AppImage
frontend/src-tauri/target/release/bundle/deb/*.deb
retention-days: 5

build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.2

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install sccache
run: |
choco install sccache --no-progress -y
sccache --version

- name: Cache sccache
uses: actions/cache@v4
with:
path: ~\AppData\Local\Mozilla\sccache
key: ${{ runner.os }}-sccache-windows-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-sccache-windows-
${{ runner.os }}-sccache-

- name: Provide ONNX Runtime (Windows)
shell: bash
run: |
ORT_VERSION=1.22.0
ORT_ROOT="$RUNNER_TEMP/onnxruntime"
mkdir -p "$ORT_ROOT"
curl -fL --retry 5 --retry-delay 2 --retry-all-errors \
"https://github.com/microsoft/onnxruntime/releases/download/v${ORT_VERSION}/onnxruntime-win-x64-${ORT_VERSION}.zip" \
-o "$ORT_ROOT/ort.zip"
unzip -q "$ORT_ROOT/ort.zip" -d "$ORT_ROOT"
echo "ORT_LIB_LOCATION=$ORT_ROOT/onnxruntime-win-x64-${ORT_VERSION}" >> "$GITHUB_ENV"
echo "ORT_SKIP_DOWNLOAD=true" >> "$GITHUB_ENV"

- name: Install frontend dependencies
working-directory: ./frontend
run: bun install

- name: Configure sccache
shell: bash
run: |
{
echo "RUSTC_WRAPPER=sccache"
echo "SCCACHE_CACHE_SIZE=2G"
} >> "$GITHUB_ENV"

# Fork PRs do not receive TAURI_SIGNING_PRIVATE_KEY. When absent, write
# an unsigned config override so tauri does not attempt updater
# artifact signing after producing the NSIS installer.
- name: Detect signing capability
id: signing
env:
KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
shell: bash
run: |
if [ -z "$KEY" ]; then
echo "skip_signing=true" >> "$GITHUB_OUTPUT"
echo "::notice::TAURI_SIGNING_PRIVATE_KEY unavailable (likely fork PR); skipping updater artifact signing"
else
echo "skip_signing=false" >> "$GITHUB_OUTPUT"
fi

- name: Write unsigned config override
if: steps.signing.outputs.skip_signing == 'true'
shell: bash
run: |
cat > frontend/src-tauri/tauri.unsigned.conf.json <<'EOF'
{
"bundle": {
"createUpdaterArtifacts": false
}
}
EOF

- name: Build Tauri App (Windows)
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
VITE_OPEN_SECRET_API_URL: ${{ github.event_name == 'pull_request' && 'https://enclave.secretgpt.ai' || 'https://enclave.trymaple.ai' }}
VITE_MAPLE_BILLING_API_URL: ${{ github.event_name == 'pull_request' && 'https://billing-dev.opensecret.cloud' || 'https://billing.opensecret.cloud' }}
VITE_CLIENT_ID: ba5a14b5-d915-47b1-b7b1-afda52bc5fc6
with:
projectPath: './frontend'
args: --bundles nsis ${{ steps.signing.outputs.skip_signing == 'true' && '--config src-tauri/tauri.unsigned.conf.json' || '' }}

- name: Show sccache stats
run: sccache --show-stats

- name: Upload Windows Build
uses: actions/upload-artifact@v4
with:
name: maple-windows-x64
path: |
frontend/src-tauri/target/release/bundle/nsis/*.exe
retention-days: 5
25 changes: 5 additions & 20 deletions frontend/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,23 @@
"fs:default",
{
"identifier": "fs:allow-read-file",
"allow": [
{ "path": "$APPCONFIG/**" },
{ "path": "$HOME/.config/maple/**" }
]
"allow": [{ "path": "$APPCONFIG/**" }]
},
{
"identifier": "fs:allow-write-file",
"allow": [
{ "path": "$APPCONFIG/**" },
{ "path": "$HOME/.config/maple/**" }
]
"allow": [{ "path": "$APPCONFIG/**" }]
},
{
"identifier": "fs:allow-create",
"allow": [
{ "path": "$APPCONFIG/**" },
{ "path": "$HOME/.config/maple/**" }
]
"allow": [{ "path": "$APPCONFIG/**" }]
},
{
"identifier": "fs:allow-exists",
"allow": [
{ "path": "$APPCONFIG/**" },
{ "path": "$HOME/.config/maple/**" }
]
"allow": [{ "path": "$APPCONFIG/**" }]
},
{
"identifier": "fs:allow-mkdir",
"allow": [
{ "path": "$APPCONFIG" },
{ "path": "$HOME/.config/maple" }
]
"allow": [{ "path": "$APPCONFIG" }]
},
{
"identifier": "opener:allow-open-url",
Expand Down
55 changes: 30 additions & 25 deletions frontend/src-tauri/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ impl ProxyState {

#[tauri::command]
pub async fn start_proxy(
app_handle: AppHandle,
state: State<'_, ProxyState>,
config: ProxyConfig,
) -> Result<ProxyStatus, String> {
Expand Down Expand Up @@ -132,7 +133,7 @@ pub async fn start_proxy(
*running = true;

// Save config to disk
if let Err(e) = save_proxy_config(&config).await {
if let Err(e) = save_proxy_config(&app_handle, &config).await {
log::error!("Failed to save proxy config: {e}");
}

Expand Down Expand Up @@ -185,15 +186,15 @@ pub async fn get_proxy_status(state: State<'_, ProxyState>) -> Result<ProxyStatu
}

#[tauri::command]
pub async fn load_proxy_config() -> Result<ProxyConfig, String> {
load_saved_proxy_config()
pub async fn load_proxy_config(app_handle: AppHandle) -> Result<ProxyConfig, String> {
load_saved_proxy_config(&app_handle)
.await
.map_err(|e| format!("Failed to load proxy config: {e}"))
}

#[tauri::command]
pub async fn save_proxy_settings(config: ProxyConfig) -> Result<(), String> {
save_proxy_config(&config)
pub async fn save_proxy_settings(app_handle: AppHandle, config: ProxyConfig) -> Result<(), String> {
save_proxy_config(&app_handle, &config)
.await
.map_err(|e| format!("Failed to save proxy config: {e}"))
}
Expand All @@ -214,29 +215,33 @@ pub async fn test_proxy_port(host: String, port: u16) -> Result<bool, String> {
}
}

// Helper functions for config persistence
async fn get_config_path() -> Result<PathBuf> {
// Use a hardcoded app name for the data directory
let app_name = "maple";
let home_dir = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map_err(|_| anyhow!("Failed to get home directory"))?;

// Note: We use ~/.config on all platforms for simplicity.
// This works well on Linux and macOS (our currently supported platforms).
// While macOS traditionally uses ~/Library/Application Support, many modern
// cross-platform tools use ~/.config on macOS as well.
// If Windows support is added in the future, consider using %APPDATA% instead.
let app_dir = PathBuf::from(home_dir).join(".config").join(app_name);
// Helper functions for config persistence.
// Epic 2 (PR 3) will unify all platforms onto app_config_dir() and add atomic
// migration + keyring-backed secret storage. This minimal arm just unblocks
// Windows compile + launch.
async fn get_config_path(app_handle: &AppHandle) -> Result<PathBuf> {
let app_dir = if cfg!(target_os = "windows") {
// Resolves to %APPDATA%\cloud.opensecret.maple\ (Roaming).
app_handle
.path()
.app_config_dir()
.map_err(|e| anyhow!("Failed to resolve app config dir: {e}"))?
} else {
// macOS/Linux: ~/.config/maple/ — unchanged for byte-identical behavior.
let app_name = "maple";
let home_dir =
std::env::var("HOME").map_err(|_| anyhow!("Failed to get home directory"))?;
PathBuf::from(home_dir).join(".config").join(app_name)
};

// Ensure directory exists
tokio::fs::create_dir_all(&app_dir).await?;

Ok(app_dir.join("proxy_config.json"))
}

async fn save_proxy_config(config: &ProxyConfig) -> Result<()> {
let path = get_config_path().await?;
async fn save_proxy_config(app_handle: &AppHandle, config: &ProxyConfig) -> Result<()> {
let path = get_config_path(app_handle).await?;
let json = serde_json::to_string_pretty(config)?;

// Write the config file
Expand All @@ -253,8 +258,8 @@ async fn save_proxy_config(config: &ProxyConfig) -> Result<()> {
Ok(())
}

async fn load_saved_proxy_config() -> Result<ProxyConfig> {
let path = get_config_path().await?;
async fn load_saved_proxy_config(app_handle: &AppHandle) -> Result<ProxyConfig> {
let path = get_config_path(app_handle).await?;

if !path.exists() {
return Ok(ProxyConfig::default());
Expand All @@ -268,7 +273,7 @@ async fn load_saved_proxy_config() -> Result<ProxyConfig> {
// Initialize proxy on app startup if auto_start is enabled
pub async fn init_proxy_on_startup_simple(app_handle: AppHandle) -> Result<()> {
// Load saved config
let config = load_saved_proxy_config().await?;
let config = load_saved_proxy_config(&app_handle).await?;

// Check if auto-start is enabled and we have an API key
if config.auto_start && !config.api_key.is_empty() {
Expand All @@ -278,7 +283,7 @@ pub async fn init_proxy_on_startup_simple(app_handle: AppHandle) -> Result<()> {
let proxy_state: tauri::State<ProxyState> = app_handle.state();

// Try to start the proxy
match start_proxy(proxy_state, config.clone()).await {
match start_proxy(app_handle.clone(), proxy_state, config.clone()).await {
Ok(_) => {
log::info!(
"Proxy auto-started successfully on {}:{}",
Expand Down
10 changes: 8 additions & 2 deletions frontend/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
},
"bundle": {
"active": true,
"targets": "all",
"targets": ["nsis", "deb", "appimage", "rpm", "dmg", "app"],
"publisher": "OpenSecret",
"icon": [
"icons/32x32.png",
Expand Down Expand Up @@ -94,7 +94,13 @@
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
"timestampUrl": "",
"webviewInstallMode": {
"type": "downloadBootstrapper"
},
"nsis": {
"installMode": "currentUser"
}
},
"createUpdaterArtifacts": true
}
Expand Down