mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[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:
@@ -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=
|
||||||
@@ -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
|
||||||
@@ -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,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 = """\
|
||||||
|
|||||||
@@ -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 |
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+22
-2
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 0–7 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
|
||||||
@@ -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 ""
|
||||||
Reference in New Issue
Block a user