[eric] windows shipping: electron-builder win+nsis + Azure Trusted Signing hook, build-app-win.ps1 / build-python-env-win.ps1 mirroring mac, release-windows.yml tag-triggered CI, icon.ico + .env.windows.example, .claude/ + .env.* gitignored. Runtime fixes: PYTHONUTF8=1 (cp1252 UnicodeDecode), python.exe / Lib\site-packages paths, PYTHONPATH path.delimiter, taskkill /T /F process tree, OAuth UA platform branch, sign-vmp postinstall via node wrapper. Backend: _resolve_command PATHEXT probing (fixes uvx not found), UV_PYTHON Windows path. Cross-env added to 9router+electron for NODE_ENV prefix on cmd. Onboarding: poll subscriptions/status while connect step is open, render Connected for active providers (was stuck on "Connect" after 30s OAuth poll timeout).

This commit is contained in:
Eric
2026-04-21 18:15:49 -07:00
parent 87e42d0a3a
commit 9ff07443db
17 changed files with 772 additions and 47 deletions
+18
View File
@@ -0,0 +1,18 @@
# Local Windows signing config — copy to .env.windows (gitignored) and fill in.
# Used by scripts/build-app-win.ps1 when running a signed local Windows build.
# CI uses GitHub Actions Secrets with the same names — see .github/workflows/release-windows.yml.
# Azure Trusted Signing — Microsoft.Trusted.Signing.Client + signtool
AZURE_TENANT_ID=
AZURE_CLIENT_ID=
AZURE_CLIENT_SECRET=
AZURE_SIGNING_ENDPOINT=https://wus2.codesigning.azure.net/
AZURE_SIGNING_ACCOUNT=mist-code-signing
AZURE_SIGNING_CERT_PROFILE=Mist-Windows-Signing
# Optional — only needed if signtool / dlib aren't auto-discovered
# SIGNTOOL_PATH=
# AZURE_SIGNING_DLIB=
# GitHub Releases publish (set if running build-app-win.ps1 -Publish)
GH_TOKEN=
+123
View File
@@ -0,0 +1,123 @@
name: Release (Windows)
# Builds + signs + uploads the Windows installer to the GitHub Release matching
# the app version in electron/package.json. Mac builds stay on the local
# publish.sh flow — this workflow is Windows-only on purpose.
#
# Triggers:
# - Push a tag `v*` (e.g. v1.0.25) → full signed release build, uploaded to
# the release of that tag (creates it in draft if absent).
# - Manual dispatch (workflow_dispatch) with `publish: false` → signed build
# as an artifact, no release upload. Useful for smoke-tests.
#
# Required repository secrets (Settings → Secrets and variables → Actions):
# AZURE_TENANT_ID
# AZURE_CLIENT_ID
# AZURE_CLIENT_SECRET
# AZURE_SIGNING_ENDPOINT e.g. https://wus2.codesigning.azure.net/
# AZURE_SIGNING_ACCOUNT mist-code-signing
# AZURE_SIGNING_CERT_PROFILE Mist-Windows-Signing
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
publish:
description: 'Publish to GitHub Releases (otherwise artifact only)'
required: true
default: 'false'
type: choice
options:
- 'false'
- 'true'
permissions:
contents: write
jobs:
build-windows:
runs-on: windows-latest
timeout-minutes: 60
env:
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }}
AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }}
AZURE_SIGNING_CERT_PROFILE: ${{ secrets.AZURE_SIGNING_CERT_PROFILE }}
PUBLISH_INPUT: ${{ github.event.inputs.publish }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup Python (for building bundled python-env)
uses: actions/setup-python@v5
with:
python-version: '3.13'
# The signing hook calls `signtool.exe` directly. signtool ships in the
# Windows 10 SDK, preinstalled on windows-latest runners — we just need
# the dlib for Azure Trusted Signing, pulled via NuGet.
- name: Install Microsoft.Trusted.Signing.Client (dlib for signtool)
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$dlibDir = Join-Path $env:GITHUB_WORKSPACE 'trusted-signing-client'
New-Item -ItemType Directory -Force -Path $dlibDir | Out-Null
nuget install Microsoft.Trusted.Signing.Client -Version 1.0.60 -OutputDirectory $dlibDir -ExcludeVersion
$dlib = Join-Path $dlibDir 'Microsoft.Trusted.Signing.Client\bin\x64\Azure.CodeSigning.Dlib.dll'
if (-not (Test-Path $dlib)) {
Get-ChildItem -Path $dlibDir -Recurse -Filter 'Azure.CodeSigning.Dlib.dll' | ForEach-Object { Write-Host "Found: $($_.FullName)" }
throw "Azure.CodeSigning.Dlib.dll not found after NuGet install"
}
"AZURE_SIGNING_DLIB=$dlib" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
Write-Host "AZURE_SIGNING_DLIB=$dlib"
- name: Locate signtool.exe on the runner
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$candidates = Get-ChildItem -Path 'C:\Program Files (x86)\Windows Kits\10\bin' -Recurse -Filter 'signtool.exe' -ErrorAction SilentlyContinue `
| Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } `
| Sort-Object FullName -Descending
if (-not $candidates) { throw "signtool.exe not found on runner" }
$signtool = $candidates[0].FullName
"SIGNTOOL_PATH=$signtool" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
Write-Host "SIGNTOOL_PATH=$signtool"
- name: Build app
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$ErrorActionPreference = 'Stop'
$shouldPublish = ($env:GITHUB_EVENT_NAME -eq 'push') -or `
($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch' -and $env:PUBLISH_INPUT -eq 'true')
if ($shouldPublish) {
Write-Host "Build mode: PUBLISH"
pwsh -NoProfile -File scripts\build-app-win.ps1 -Publish
} else {
Write-Host "Build mode: SIGN (artifact only)"
pwsh -NoProfile -File scripts\build-app-win.ps1 -Sign
}
if ($LASTEXITCODE -ne 0) { throw "build-app-win.ps1 failed ($LASTEXITCODE)" }
- name: Upload artifact (non-publish runs)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.publish != 'true'
uses: actions/upload-artifact@v4
with:
name: openswarm-windows-x64
path: |
electron/dist/*.exe
electron/dist/latest.yml
if-no-files-found: error
retention-days: 14
+2
View File
@@ -1,6 +1,8 @@
.DS_Store .DS_Store
.worktrees .worktrees
.env .env
.env.*
!.env*.example
.local-stash/ .local-stash/
backend/data/** backend/data/**
!backend/data/outputs/ !backend/data/outputs/
+5 -4
View File
@@ -5,11 +5,11 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --webpack --port 20128", "dev": "next dev --webpack --port 20128",
"build": "NODE_ENV=production next build --webpack", "build": "cross-env NODE_ENV=production next build --webpack",
"start": "NODE_ENV=production next start", "start": "cross-env NODE_ENV=production next start",
"dev:bun": "bun --bun next dev --webpack --port 20128", "dev:bun": "bun --bun next dev --webpack --port 20128",
"build:bun": "NODE_ENV=production bun --bun next build --webpack", "build:bun": "cross-env NODE_ENV=production bun --bun next build --webpack",
"start:bun": "NODE_ENV=production bun ./.next/standalone/server.js" "start:bun": "cross-env NODE_ENV=production bun ./.next/standalone/server.js"
}, },
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
@@ -40,6 +40,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"cross-env": "^7.0.3",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"postcss": "^8.5.6", "postcss": "^8.5.6",
@@ -4,7 +4,7 @@ import os
_SKILL_PATH = os.path.join(os.path.dirname(__file__), "view_builder_skill.md") _SKILL_PATH = os.path.join(os.path.dirname(__file__), "view_builder_skill.md")
with open(_SKILL_PATH) as _f: with open(_SKILL_PATH, encoding="utf-8") as _f:
VIEW_BUILDER_SKILL = _f.read() VIEW_BUILDER_SKILL = _f.read()
VIEW_TEMPLATE_INDEX = """\ VIEW_TEMPLATE_INDEX = """\
+27 -17
View File
@@ -6,6 +6,7 @@ import re
import logging import logging
import secrets import secrets
import shutil import shutil
import sys
import time import time
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import Any, Optional from typing import Any, Optional
@@ -415,24 +416,26 @@ def _resolve_command(command: str) -> str | None:
found = shutil.which(command) found = shutil.which(command)
if found: if found:
return found return found
# Windows binaries need an extension. shutil.which() handles PATHEXT for
# PATH lookups, but we manually scan _extra_bin_dirs below — replicate
# the suffix probing here so `uvx` finds `uvx.exe`, etc.
if sys.platform == "win32":
suffixes = [""] + os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").lower().split(os.pathsep)
else:
suffixes = [""]
def _probe(directory: str) -> str | None:
for suffix in suffixes:
candidate = os.path.join(directory, command + suffix)
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
return None
for d in _extra_bin_dirs(): for d in _extra_bin_dirs():
candidate = os.path.join(d, command) hit = _probe(d)
if os.path.isfile(candidate) and os.access(candidate, os.X_OK): if hit:
return candidate return hit
# Check bundled uv-bin directory (ships uv/uvx for non-dev users) # Check bundled uv-bin directory (ships uv/uvx for non-dev users)
_backend = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) _backend = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
_is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1" return _probe(os.path.join(_backend, "uv-bin"))
if _is_packaged:
# In packaged app: <resources>/backend/uv-bin/
candidate = os.path.join(_backend, "uv-bin", command)
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
else:
# In dev: backend/uv-bin/
candidate = os.path.join(_backend, "uv-bin", command)
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
return None
def _augmented_path() -> str: def _augmented_path() -> str:
@@ -552,14 +555,21 @@ def derive_mcp_config(tool: ToolDefinition) -> Optional[dict]:
# Point uv/uvx at our bundled Python — avoids macOS CLT popup on fresh Macs # Point uv/uvx at our bundled Python — avoids macOS CLT popup on fresh Macs
# and avoids downloading Python at runtime # and avoids downloading Python at runtime
_is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1" _is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1"
_is_windows = sys.platform == "win32"
if _is_packaged: if _is_packaged:
_resources = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) _resources = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
_bundled_python = os.path.join(_resources, "python-env", "bin", "python3") if _is_windows:
_bundled_python = os.path.join(_resources, "python-env", "python.exe")
else:
_bundled_python = os.path.join(_resources, "python-env", "bin", "python3")
if os.path.exists(_bundled_python): if os.path.exists(_bundled_python):
env.setdefault("UV_PYTHON", _bundled_python) env.setdefault("UV_PYTHON", _bundled_python)
else: else:
_backend = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) _backend = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
_venv_python = os.path.join(_backend, ".venv", "bin", "python3") if _is_windows:
_venv_python = os.path.join(_backend, ".venv", "Scripts", "python.exe")
else:
_venv_python = os.path.join(_backend, ".venv", "bin", "python3")
if os.path.exists(_venv_python): if os.path.exists(_venv_python):
env.setdefault("UV_PYTHON", _venv_python) env.setdefault("UV_PYTHON", _venv_python)
@@ -1,7 +1,6 @@
{ {
"name": "notionhq-notion-mcp-server", "name": "notionhq-notion-mcp-server",
"version": "1.0.0", "version": "1.0.0",
"description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@@ -12,5 +11,6 @@
"type": "commonjs", "type": "commonjs",
"dependencies": { "dependencies": {
"@notionhq/notion-mcp-server": "^2.2.1" "@notionhq/notion-mcp-server": "^2.2.1"
} },
"description": ""
} }
@@ -1,7 +1,6 @@
{ {
"name": "softeria-ms-365-mcp-server", "name": "softeria-ms-365-mcp-server",
"version": "1.0.0", "version": "1.0.0",
"description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@@ -12,5 +11,6 @@
"type": "commonjs", "type": "commonjs",
"dependencies": { "dependencies": {
"@softeria/ms-365-mcp-server": "^0.79.6" "@softeria/ms-365-mcp-server": "^0.79.6"
} },
"description": ""
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

+95
View File
@@ -0,0 +1,95 @@
// Custom Windows signing hook for electron-builder.
// Uses Azure Trusted Signing via signtool + Microsoft.Trusted.Signing.Client dlib.
//
// Required env vars (set as GitHub Actions secrets and passed through to the job):
// AZURE_TENANT_ID Directory (tenant) ID of the app registration
// AZURE_CLIENT_ID Application (client) ID of the app registration
// AZURE_CLIENT_SECRET Client secret value
// AZURE_SIGNING_ENDPOINT e.g. https://eus.codesigning.azure.net
// AZURE_SIGNING_ACCOUNT Trusted Signing account name (e.g. mist-code-signing)
// AZURE_SIGNING_CERT_PROFILE Certificate profile name (created inside the account)
//
// Optional env vars:
// AZURE_SIGNING_DLIB Absolute path to Azure.CodeSigning.Dlib.dll (default: resolved from AZURE_SIGNING_DLIB_DIR)
// AZURE_SIGNING_DLIB_DIR Directory containing Azure.CodeSigning.Dlib.dll (default: ./Microsoft.Trusted.Signing.Client/bin/x64)
// SIGNTOOL_PATH Absolute path to signtool.exe (default: "signtool", relying on PATH)
// AZURE_TIMESTAMP_URL Timestamp server (default: http://timestamp.acs.microsoft.com)
// CSC_IDENTITY_AUTO_DISCOVERY Set to "false" to skip signing entirely (dev builds)
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const REQUIRED_ENV = [
'AZURE_TENANT_ID',
'AZURE_CLIENT_ID',
'AZURE_CLIENT_SECRET',
'AZURE_SIGNING_ENDPOINT',
'AZURE_SIGNING_ACCOUNT',
'AZURE_SIGNING_CERT_PROFILE',
];
function resolveDlib() {
if (process.env.AZURE_SIGNING_DLIB) return process.env.AZURE_SIGNING_DLIB;
const dir = process.env.AZURE_SIGNING_DLIB_DIR
|| path.join(process.cwd(), 'Microsoft.Trusted.Signing.Client', 'bin', 'x64');
return path.join(dir, 'Azure.CodeSigning.Dlib.dll');
}
exports.default = async function signWindows(configuration) {
const targetPath = configuration && configuration.path;
if (!targetPath) {
console.log('[sign-windows] No target path provided — skipping');
return;
}
if (process.env.CSC_IDENTITY_AUTO_DISCOVERY === 'false') {
console.log(`[sign-windows] CSC_IDENTITY_AUTO_DISCOVERY=false — skipping ${targetPath}`);
return;
}
const missing = REQUIRED_ENV.filter((k) => !process.env[k]);
if (missing.length) {
console.log(`[sign-windows] Skipping ${targetPath} — missing env: ${missing.join(', ')}`);
return;
}
const dlib = resolveDlib();
if (!fs.existsSync(dlib)) {
throw new Error(
`[sign-windows] Azure.CodeSigning.Dlib.dll not found at ${dlib}. `
+ `Install the Microsoft.Trusted.Signing.Client NuGet package or set AZURE_SIGNING_DLIB.`,
);
}
const metadata = {
Endpoint: process.env.AZURE_SIGNING_ENDPOINT,
CodeSigningAccountName: process.env.AZURE_SIGNING_ACCOUNT,
CertificateProfileName: process.env.AZURE_SIGNING_CERT_PROFILE,
ExcludeEnvironmentCredential: 'false',
};
const metadataPath = path.join(os.tmpdir(), `ts-metadata-${process.pid}-${Date.now()}.json`);
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
const signtool = process.env.SIGNTOOL_PATH || 'signtool.exe';
const timestampUrl = process.env.AZURE_TIMESTAMP_URL || 'http://timestamp.acs.microsoft.com';
const args = [
'sign',
'/v',
'/fd', 'SHA256',
'/tr', timestampUrl,
'/td', 'SHA256',
'/dlib', dlib,
'/dmdf', metadataPath,
targetPath,
];
try {
console.log(`[sign-windows] signtool ${args.join(' ')}`);
execFileSync(signtool, args, { stdio: 'inherit' });
console.log(`[sign-windows] Signed ${targetPath}`);
} finally {
try { fs.unlinkSync(metadataPath); } catch (_) { /* ignore */ }
}
};
+47 -16
View File
@@ -79,7 +79,9 @@ let cachedUpdateStatus = { status: 'idle', info: null, error: null };
const isPackaged = app.isPackaged; const isPackaged = app.isPackaged;
const isDev = process.env.ELECTRON_DEV === '1'; const isDev = process.env.ELECTRON_DEV === '1';
const iconPath = path.join(__dirname, 'build', 'icon.png'); const iconPath = process.platform === 'win32'
? path.join(__dirname, 'build', 'icon.ico')
: path.join(__dirname, 'build', 'icon.png');
/** /**
* macOS GUI apps launched from Finder/Dock inherit a minimal PATH from launchd * macOS GUI apps launched from Finder/Dock inherit a minimal PATH from launchd
@@ -164,12 +166,20 @@ function getResourcePath(...segments) {
} }
function getPythonPath() { function getPythonPath() {
// python-build-standalone layout differs by OS:
// macOS / Linux: <env>/bin/python3
// Windows: <env>\python.exe (no bin/, no python3)
if (isPackaged) { if (isPackaged) {
const envPath = path.join(process.resourcesPath, 'python-env'); const envPath = path.join(process.resourcesPath, 'python-env');
if (process.platform === 'win32') {
return path.join(envPath, 'python.exe');
}
return path.join(envPath, 'bin', 'python3'); return path.join(envPath, 'bin', 'python3');
} }
const venvPython = path.join(__dirname, '..', 'backend', '.venv', 'bin', 'python3'); if (process.platform === 'win32') {
return venvPython; return path.join(__dirname, '..', 'backend', '.venv', 'Scripts', 'python.exe');
}
return path.join(__dirname, '..', 'backend', '.venv', 'bin', 'python3');
} }
function waitForBackend(port, timeoutMs = 60000) { function waitForBackend(port, timeoutMs = 60000) {
@@ -212,15 +222,19 @@ async function startBackend() {
OPENSWARM_PORT: String(backendPort), OPENSWARM_PORT: String(backendPort),
OPENSWARM_ELECTRON_PATH: process.execPath, OPENSWARM_ELECTRON_PATH: process.execPath,
PYTHONDONTWRITEBYTECODE: '1', PYTHONDONTWRITEBYTECODE: '1',
// PEP 540 UTF-8 mode: makes open() default to UTF-8 on Windows where
// the locale is otherwise cp1252. Many backend modules read UTF-8
// .md / .json files without an explicit encoding= argument.
PYTHONUTF8: '1',
}; };
if (isPackaged) { if (isPackaged) {
const pythonEnvSitePackages = path.join( // site-packages location differs by OS — Windows has no lib/python3.13/.
process.resourcesPath, 'python-env', 'lib', const pythonEnvSitePackages = process.platform === 'win32'
'python3.13', 'site-packages' ? path.join(process.resourcesPath, 'python-env', 'Lib', 'site-packages')
); : path.join(process.resourcesPath, 'python-env', 'lib', 'python3.13', 'site-packages');
const debuggerDir = getResourcePath('debugger'); const debuggerDir = getResourcePath('debugger');
env.PYTHONPATH = [projectRoot, debuggerDir, pythonEnvSitePackages].join(':'); env.PYTHONPATH = [projectRoot, debuggerDir, pythonEnvSitePackages].join(path.delimiter);
} }
console.log(`Starting backend: ${pythonPath} on port ${backendPort}`); console.log(`Starting backend: ${pythonPath} on port ${backendPort}`);
@@ -369,12 +383,27 @@ function setupAutoUpdater() {
function killBackend() { function killBackend() {
if (backendProcess) { if (backendProcess) {
console.log('Killing backend process...'); console.log('Killing backend process...');
backendProcess.kill('SIGTERM'); if (process.platform === 'win32') {
setTimeout(() => { // Windows: Node's child.kill() only terminates the direct child, leaving
if (backendProcess && !backendProcess.killed) { // grandchildren (e.g. the 9router node process the Python backend
backendProcess.kill('SIGKILL'); // spawned) as orphans. Use `taskkill /T /F` to walk the process tree.
try {
require('child_process').execFileSync(
'taskkill', ['/PID', String(backendProcess.pid), '/T', '/F'],
{ stdio: 'ignore' },
);
} catch (_) {
// taskkill failed (process may have already exited) — fall back to kill().
try { backendProcess.kill(); } catch (_) {}
} }
}, 3000); } else {
backendProcess.kill('SIGTERM');
setTimeout(() => {
if (backendProcess && !backendProcess.killed) {
backendProcess.kill('SIGKILL');
}
}, 3000);
}
backendProcess = null; backendProcess = null;
} }
} }
@@ -497,9 +526,11 @@ app.on('web-contents-created', (_event, contents) => {
mainWindow && mainWindow &&
contents !== mainWindow.webContents contents !== mainWindow.webContents
) { ) {
const OAUTH_POPUP_UA = const OAUTH_POPUP_UA = process.platform === 'win32'
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' + ? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
contents.setUserAgent(OAUTH_POPUP_UA); contents.setUserAgent(OAUTH_POPUP_UA);
} }
+22 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "openswarm", "name": "openswarm",
"version": "1.0.23", "version": "1.0.24",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "openswarm", "name": "openswarm",
"version": "1.0.23", "version": "1.0.24",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"electron-updater": "^6.3.0", "electron-updater": "^6.3.0",
@@ -14,6 +14,7 @@
}, },
"devDependencies": { "devDependencies": {
"@electron/notarize": "^3.1.1", "@electron/notarize": "^3.1.1",
"cross-env": "^7.0.3",
"electron": "castlabs/electron-releases#v40.7.0+wvcus", "electron": "castlabs/electron-releases#v40.7.0+wvcus",
"electron-builder": "^25.1.0" "electron-builder": "^25.1.0"
} }
@@ -1572,6 +1573,25 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
},
"bin": {
"cross-env": "src/bin/cross-env.js",
"cross-env-shell": "src/bin/cross-env-shell.js"
},
"engines": {
"node": ">=10.14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"dev": true, "dev": true,
+29 -3
View File
@@ -5,11 +5,13 @@
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"dev": "ELECTRON_DEV=1 electron .", "dev": "cross-env ELECTRON_DEV=1 electron .",
"postinstall": "bash scripts/sign-vmp.sh", "postinstall": "node scripts/sign-vmp.js",
"sign-vmp": "bash scripts/sign-vmp.sh", "sign-vmp": "node scripts/sign-vmp.js",
"dist": "electron-builder --mac --publish never", "dist": "electron-builder --mac --publish never",
"dist:publish": "electron-builder --mac --publish always", "dist:publish": "electron-builder --mac --publish always",
"dist:win": "electron-builder --win --x64 --publish never",
"dist:win:publish": "electron-builder --win --x64 --publish always",
"dist:all": "electron-builder --mac --win --linux" "dist:all": "electron-builder --mac --win --linux"
}, },
"dependencies": { "dependencies": {
@@ -18,6 +20,7 @@
}, },
"devDependencies": { "devDependencies": {
"@electron/notarize": "^3.1.1", "@electron/notarize": "^3.1.1",
"cross-env": "^7.0.3",
"electron": "castlabs/electron-releases#v40.7.0+wvcus", "electron": "castlabs/electron-releases#v40.7.0+wvcus",
"electron-builder": "^25.1.0" "electron-builder": "^25.1.0"
}, },
@@ -58,6 +61,29 @@
} }
] ]
}, },
"win": {
"icon": "build/icon.ico",
"target": [
{
"target": "nsis",
"arch": ["x64"]
}
],
"artifactName": "OpenSwarm-Setup-${version}-${arch}.${ext}",
"sign": "./build/sign-windows.js",
"signingHashAlgorithms": ["sha256"],
"signDlls": false
},
"nsis": {
"oneClick": true,
"perMachine": false,
"allowToChangeInstallationDirectory": false,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "OpenSwarm",
"deleteAppDataOnUninstall": false,
"artifactName": "OpenSwarm-Setup-${version}-${arch}.${ext}"
},
"extraResources": [ "extraResources": [
{ {
"from": "build-staging/frontend", "from": "build-staging/frontend",
+14
View File
@@ -0,0 +1,14 @@
// Cross-platform wrapper around sign-vmp.sh.
// VMP signing (CastLabs Widevine) only runs on macOS. On Windows/Linux this
// is a no-op so `npm install` doesn't blow up trying to invoke bash.
const { spawnSync } = require('child_process');
const path = require('path');
if (process.platform !== 'darwin') {
console.log(`[vmp] Skipping VMP signing on ${process.platform} (macOS-only)`);
process.exit(0);
}
const script = path.join(__dirname, 'sign-vmp.sh');
const result = spawnSync('bash', [script], { stdio: 'inherit' });
process.exit(result.status ?? 0);
@@ -135,9 +135,33 @@ const OnboardingModal: React.FC = () => {
const [referralSourceOther, setReferralSourceOther] = useState<string>(''); const [referralSourceOther, setReferralSourceOther] = useState<string>('');
const [connecting, setConnecting] = useState<string | null>(null); const [connecting, setConnecting] = useState<string | null>(null);
const [nineRouterReady, setNineRouterReady] = useState<boolean | null>(null); const [nineRouterReady, setNineRouterReady] = useState<boolean | null>(null);
// Providers the backend says are already authenticated. Used to show a
// "Connected" state on rows where the user has already linked the account.
// Without this the row reverts to "Connect ->" after a 30s OAuth timeout
// even when the backend actually completed the link.
const [connectedProviders, setConnectedProviders] = useState<Set<string>>(new Set());
const pollTimerRef = useRef<any>(null); const pollTimerRef = useRef<any>(null);
const msgHandlerRef = useRef<any>(null); const msgHandlerRef = useRef<any>(null);
// Poll subscription status while the connect step is showing so the row
// labels reflect any post-timeout backend success and any prior connections.
useEffect(() => {
if (step !== 'connect' || !open) return;
let cancelled = false;
const refresh = async () => {
try {
const r = await fetch(`${API_BASE}/agents/subscriptions/status`);
const d = await r.json();
if (cancelled) return;
const conns = d?.providers?.connections || [];
setConnectedProviders(new Set(conns.filter((p: any) => p.isActive).map((p: any) => p.provider)));
} catch {}
};
refresh();
const id = setInterval(refresh, 4000);
return () => { cancelled = true; clearInterval(id); };
}, [step, open]);
// Poll for 9Router readiness (it may still be starting when onboarding shows) // Poll for 9Router readiness (it may still be starting when onboarding shows)
useEffect(() => { useEffect(() => {
let attempts = 0; let attempts = 0;
+261
View File
@@ -0,0 +1,261 @@
# Master build script for the OpenSwarm desktop app on Windows.
#
# Usage:
# pwsh scripts\build-app-win.ps1 Local dev build (unsigned)
# pwsh scripts\build-app-win.ps1 -Sign Signed build (no publish)
# pwsh scripts\build-app-win.ps1 -Publish Production build (sign + publish to GitHub Releases)
#
# Reads .env.windows (gitignored) for Azure Trusted Signing + GH_TOKEN if -Sign or -Publish.
[CmdletBinding()]
param(
[switch]$Sign,
[switch]$Publish
)
$ErrorActionPreference = 'Stop'
if ($Publish) { $Sign = $true }
$ScriptDir = Split-Path -Parent $PSCommandPath
$ProjectRoot = Split-Path -Parent $ScriptDir
# --- Load .env.windows if present ---
$EnvFile = Join-Path $ProjectRoot '.env.windows'
if (Test-Path $EnvFile) {
Get-Content $EnvFile | ForEach-Object {
$line = $_.Trim()
if ($line -and -not $line.StartsWith('#') -and $line.Contains('=')) {
$idx = $line.IndexOf('=')
$name = $line.Substring(0, $idx).Trim()
$value = $line.Substring($idx + 1).Trim()
if ($value.StartsWith('"') -and $value.EndsWith('"')) {
$value = $value.Substring(1, $value.Length - 2)
}
Set-Item -Path "Env:$name" -Value $value
}
}
}
Write-Host "========================================"
Write-Host " OpenSwarm Desktop App Builder (Windows)"
if ($Publish) { Write-Host " Mode: PRODUCTION (sign + publish to GitHub Releases)" }
elseif ($Sign) { Write-Host " Mode: SIGNED (sign, no publish)" }
else { Write-Host " Mode: LOCAL (unsigned)" }
Write-Host "========================================"
Write-Host ""
# --- Required env validation ---
if ($Sign) {
$required = @(
'AZURE_TENANT_ID','AZURE_CLIENT_ID','AZURE_CLIENT_SECRET',
'AZURE_SIGNING_ENDPOINT','AZURE_SIGNING_ACCOUNT','AZURE_SIGNING_CERT_PROFILE'
)
if ($Publish) { $required += 'GH_TOKEN' }
$missing = $required | Where-Object { -not [Environment]::GetEnvironmentVariable($_) }
if ($missing.Count -gt 0) {
Write-Host "ERROR: Missing required environment variables:" -ForegroundColor Red
$missing | ForEach-Object { Write-Host " - $_" }
Write-Host "Copy .env.windows.example to .env.windows and fill in values."
exit 1
}
}
# --- Step 0: Bundled uv/uvx (Windows zip) ---
$UvBinDir = Join-Path $ProjectRoot 'backend\uv-bin'
if (-not (Test-Path (Join-Path $UvBinDir 'uvx.exe'))) {
Write-Host "[0] Downloading uv/uvx for Windows..."
New-Item -ItemType Directory -Force -Path $UvBinDir | Out-Null
$UvUrl = 'https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-pc-windows-msvc.zip'
$TmpZip = Join-Path $env:TEMP "uv-win-$([guid]::NewGuid()).zip"
$TmpExtract = Join-Path $env:TEMP "uv-win-extract-$([guid]::NewGuid())"
try {
Invoke-WebRequest -Uri $UvUrl -OutFile $TmpZip -UseBasicParsing
Expand-Archive -Path $TmpZip -DestinationPath $TmpExtract -Force
Get-ChildItem -Path $TmpExtract -Recurse -Filter 'uv.exe' | Select-Object -First 1 | ForEach-Object { Copy-Item $_.FullName (Join-Path $UvBinDir 'uv.exe') -Force }
Get-ChildItem -Path $TmpExtract -Recurse -Filter 'uvx.exe' | Select-Object -First 1 | ForEach-Object { Copy-Item $_.FullName (Join-Path $UvBinDir 'uvx.exe') -Force }
Write-Host "uv/uvx downloaded and bundled."
} finally {
Remove-Item -Force $TmpZip -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force $TmpExtract -ErrorAction SilentlyContinue
}
} else {
Write-Host "[0] uv/uvx binaries already present."
}
Write-Host ""
# --- Step 0b: Bundle reddit-mcp-buddy via esbuild ---
$McpBundleDir = Join-Path $ProjectRoot 'backend\mcp-bundles'
New-Item -ItemType Directory -Force -Path $McpBundleDir | Out-Null
$RedditBundle = Join-Path $McpBundleDir 'reddit-mcp-buddy.js'
if (-not (Test-Path $RedditBundle)) {
Write-Host "[0b] Bundling reddit-mcp-buddy..."
$TmpDir = Join-Path $env:TEMP "openswarm-mcp-$([guid]::NewGuid())"
New-Item -ItemType Directory -Force -Path $TmpDir | Out-Null
Push-Location $TmpDir
try {
& npm install reddit-mcp-buddy --silent 2>$null
& npx esbuild "node_modules/reddit-mcp-buddy/dist/index.js" --bundle --platform=node --format=cjs "--outfile=$RedditBundle"
if ($LASTEXITCODE -ne 0) { throw "esbuild failed" }
Write-Host "reddit-mcp-buddy bundled."
} finally {
Pop-Location
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
}
} else {
Write-Host "[0b] reddit-mcp-buddy bundle already present."
}
# --- Step 0c: npm MCP servers that can't be bundled ---
$NpmServersDir = Join-Path $ProjectRoot 'backend\npm-servers'
New-Item -ItemType Directory -Force -Path $NpmServersDir | Out-Null
function Install-NpmServer($SubDir, $PackageName) {
$TargetDir = Join-Path $NpmServersDir $SubDir
$NodeModules = Join-Path $TargetDir 'node_modules'
if (Test-Path $NodeModules) {
Write-Host "[0c] $PackageName already present."
return
}
Write-Host "[0c] Installing $PackageName..."
New-Item -ItemType Directory -Force -Path $TargetDir | Out-Null
Push-Location $TargetDir
try {
& npm init -y *>$null
& npm install $PackageName --silent 2>$null
if ($LASTEXITCODE -ne 0) { throw "$PackageName install failed" }
Write-Host "$PackageName installed."
} finally {
Pop-Location
}
}
Install-NpmServer 'softeria-ms-365-mcp-server' '@softeria/ms-365-mcp-server'
Install-NpmServer 'notionhq-notion-mcp-server' '@notionhq/notion-mcp-server'
Write-Host ""
# --- Step 1: Frontend build ---
Write-Host "[1/5] Building frontend..."
Push-Location (Join-Path $ProjectRoot 'frontend')
try {
& npm install
if ($LASTEXITCODE -ne 0) { throw "npm install (frontend) failed" }
& npm run build
if ($LASTEXITCODE -ne 0) { throw "frontend build failed" }
} finally { Pop-Location }
if (-not (Test-Path (Join-Path $ProjectRoot 'frontend\dist\index.html'))) {
throw "Frontend build failed - dist\index.html not found"
}
Write-Host "Frontend build complete."
Write-Host ""
# --- Step 2: Python env ---
$PythonEnv = Join-Path $ProjectRoot 'electron\python-env'
$PythonExe = Join-Path $PythonEnv 'python.exe'
if ((Test-Path $PythonExe) -and -not $env:OPENSWARM_REBUILD_PYTHON) {
Write-Host "[2/5] Python environment already present at $PythonEnv (set `$env:OPENSWARM_REBUILD_PYTHON='1' to force rebuild)."
} else {
Write-Host "[2/5] Building Python environment..."
& (Join-Path $ScriptDir 'build-python-env-win.ps1')
if ($LASTEXITCODE -ne 0) { throw "Python env build failed" }
}
if (-not (Test-Path (Join-Path $ProjectRoot 'electron\python-env'))) {
throw "Python environment not found at electron\python-env\"
}
Write-Host "Python environment ready."
Write-Host ""
# --- Step 3: 9Router build ---
Write-Host "[3/5] Building 9Router..."
Push-Location (Join-Path $ProjectRoot '9router')
try {
& npm install
if ($LASTEXITCODE -ne 0) { throw "npm install (9router) failed" }
& npm run build
if ($LASTEXITCODE -ne 0) { throw "9router build failed" }
} finally { Pop-Location }
$Standalone = Join-Path $ProjectRoot '9router\.next\standalone'
if (-not (Test-Path $Standalone)) { throw "9Router build failed - .next\standalone not found" }
$NextStatic = Join-Path $ProjectRoot '9router\.next\static'
if (Test-Path $NextStatic) {
$StandaloneStatic = Join-Path $Standalone '.next\static'
New-Item -ItemType Directory -Force -Path (Split-Path $StandaloneStatic -Parent) | Out-Null
Copy-Item -Recurse -Force $NextStatic $StandaloneStatic
}
$NextPublic = Join-Path $ProjectRoot '9router\public'
if (Test-Path $NextPublic) {
Copy-Item -Recurse -Force $NextPublic (Join-Path $Standalone 'public')
}
Write-Host "9Router build complete."
Write-Host ""
# --- Step 4: Snapshot source dirs into electron\build-staging\ ---
Write-Host "[4/5] Snapshotting source directories..."
$Staging = Join-Path $ProjectRoot 'electron\build-staging'
if (Test-Path $Staging) { Remove-Item -Recurse -Force $Staging }
New-Item -ItemType Directory -Force -Path $Staging | Out-Null
function Copy-Excluded($Source, $Dest, $Exclude) {
# robocopy: built-in, fast, handles long paths.
$args = @($Source, $Dest, '/E', '/NJH', '/NJS', '/NDL', '/NFL', '/NP', '/MT:8')
foreach ($d in $Exclude.Dirs) { $args += '/XD'; $args += $d }
foreach ($f in $Exclude.Files) { $args += '/XF'; $args += $f }
& robocopy @args | Out-Null
# robocopy exit codes 07 are success
if ($LASTEXITCODE -ge 8) { throw "robocopy failed ($Source -> $Dest, exit $LASTEXITCODE)" }
$global:LASTEXITCODE = 0
}
Copy-Excluded `
(Join-Path $ProjectRoot 'backend') (Join-Path $Staging 'backend') `
@{ Dirs = @('__pycache__','.venv','tools'); Files = @('*.pyc') }
New-Item -ItemType Directory -Force -Path (Join-Path $Staging 'backend\data\tools') | Out-Null
Copy-Excluded `
(Join-Path $ProjectRoot 'debugger') (Join-Path $Staging 'debugger') `
@{ Dirs = @('__pycache__','.venv','node_modules'); Files = @('*.pyc') }
Copy-Item -Recurse -Force (Join-Path $ProjectRoot 'frontend\dist\*') (New-Item -ItemType Directory -Force -Path (Join-Path $Staging 'frontend')).FullName
# 9Router standalone -> staging\9router
Copy-Excluded $Standalone (Join-Path $Staging '9router') @{ Dirs = @(); Files = @() }
if (Test-Path $NextStatic) {
$TargetStatic = Join-Path $Staging '9router\.next\static'
New-Item -ItemType Directory -Force -Path (Split-Path $TargetStatic -Parent) | Out-Null
Copy-Item -Recurse -Force $NextStatic $TargetStatic
}
Write-Host ""
Write-Host "========================================" -BackgroundColor Green -ForegroundColor White
Write-Host " SOURCE SNAPSHOT COMPLETE " -BackgroundColor Green -ForegroundColor White
Write-Host " Safe to modify your codebase now. " -BackgroundColor Green -ForegroundColor White
Write-Host "========================================" -BackgroundColor Green -ForegroundColor White
Write-Host ""
# --- Step 5: Package with electron-builder ---
Write-Host "[5/5] Packaging with electron-builder..."
Push-Location (Join-Path $ProjectRoot 'electron')
try {
& npm install
if ($LASTEXITCODE -ne 0) { throw "npm install (electron) failed" }
if (-not $Sign) {
$env:CSC_IDENTITY_AUTO_DISCOVERY = 'false'
}
if ($Publish) {
& npx electron-builder --win --x64 --publish always
} else {
& npx electron-builder --win --x64 --publish never
}
if ($LASTEXITCODE -ne 0) { throw "electron-builder failed" }
} finally { Pop-Location }
Remove-Item -Recurse -Force $Staging -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "========================================"
Write-Host " Build Complete!"
Write-Host "========================================"
Write-Host ""
Write-Host "Output files:"
Get-ChildItem -Path (Join-Path $ProjectRoot 'electron\dist') -Filter '*.exe' -ErrorAction SilentlyContinue | Format-Table Name, Length, LastWriteTime
Get-ChildItem -Path (Join-Path $ProjectRoot 'electron\dist') -Filter '*.zip' -ErrorAction SilentlyContinue | Format-Table Name, Length, LastWriteTime
+100
View File
@@ -0,0 +1,100 @@
# Build an embedded Windows Python environment for the Electron app.
#
# Downloads a standalone CPython build for Windows from python-build-standalone,
# installs backend deps, and leaves it under electron\python-env\.
# Bundled into the .exe installer by electron-builder via extraResources.
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $PSCommandPath
$ProjectRoot = Split-Path -Parent $ScriptDir
$ElectronDir = Join-Path $ProjectRoot 'electron'
$PythonEnvDir = Join-Path $ElectronDir 'python-env'
$PythonVersion = '3.13'
$PythonFullVersion = '3.13.2'
$ReleaseTag = '20250212'
$PlatformTag = 'x86_64-pc-windows-msvc-shared'
$Tarball = "cpython-$PythonFullVersion+$ReleaseTag-$PlatformTag-install_only_stripped.tar.gz"
$DownloadUrl = "https://github.com/indygreg/python-build-standalone/releases/download/$ReleaseTag/$Tarball"
Write-Host "=== Building Windows Python Environment ==="
Write-Host "Architecture: x64 ($PlatformTag)"
Write-Host "Python: $PythonFullVersion"
if (Test-Path $PythonEnvDir) {
Write-Host "Removing old python-env..."
Remove-Item -Recurse -Force $PythonEnvDir
}
$TempDir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "openswarm-pyenv-$([guid]::NewGuid())") -Force
try {
$ArchivePath = Join-Path $TempDir.FullName 'python.tar.gz'
Write-Host "Downloading standalone Python..."
Write-Host "URL: $DownloadUrl"
Invoke-WebRequest -Uri $DownloadUrl -OutFile $ArchivePath -UseBasicParsing
Write-Host "Extracting..."
# tar is built-in on Windows 10+ (bsdtar) and handles .tar.gz natively.
& tar -xzf $ArchivePath -C $TempDir.FullName
if ($LASTEXITCODE -ne 0) { throw "tar extract failed" }
$Extracted = Join-Path $TempDir.FullName 'python'
if (-not (Test-Path $Extracted)) {
Get-ChildItem $TempDir.FullName | Format-Table | Out-String | Write-Host
throw "Expected extracted directory at $Extracted"
}
Move-Item -Path $Extracted -Destination $PythonEnvDir
Write-Host "Python installed to $PythonEnvDir"
} finally {
if (Test-Path $TempDir.FullName) {
Remove-Item -Recurse -Force $TempDir.FullName
}
}
$PythonBin = Join-Path $PythonEnvDir 'python.exe'
if (-not (Test-Path $PythonBin)) { throw "python.exe not found at $PythonBin" }
Write-Host "Python binary: $PythonBin"
& $PythonBin --version
# Ensure pip is present
& $PythonBin -m pip --version 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Host "Installing pip..."
& $PythonBin -m ensurepip --upgrade
if ($LASTEXITCODE -ne 0) { throw "ensurepip failed" }
}
Write-Host "Installing backend dependencies..."
& $PythonBin -m pip install --upgrade pip
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed" }
& $PythonBin -m pip install -r (Join-Path $ProjectRoot 'backend\requirements.txt')
if ($LASTEXITCODE -ne 0) { throw "pip install requirements failed" }
Write-Host "Installing debugger module..."
& $PythonBin -m pip install (Join-Path $ProjectRoot 'debugger')
if ($LASTEXITCODE -ne 0) { throw "pip install debugger failed" }
Write-Host "Verifying claude-agent-sdk..."
& $PythonBin -c "import claude_agent_sdk; print('claude-agent-sdk installed')"
if ($LASTEXITCODE -ne 0) { throw "claude-agent-sdk verification failed" }
# Cleanup
Write-Host "Cleaning up..."
Get-ChildItem -Path $PythonEnvDir -Recurse -Force -Directory `
| Where-Object { $_.Name -in @('__pycache__','tests','test') } `
| Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
Get-ChildItem -Path $PythonEnvDir -Recurse -Force -Filter '*.pyc' `
| Remove-Item -Force -ErrorAction SilentlyContinue
$Size = (Get-ChildItem -Path $PythonEnvDir -Recurse -File `
| Measure-Object -Property Length -Sum).Sum
$SizeMB = [math]::Round($Size / 1MB, 1)
Write-Host ""
Write-Host "=== Python Environment Ready ==="
Write-Host "Location: $PythonEnvDir"
Write-Host ("Size: {0} MB" -f $SizeMB)
Write-Host ""