diff --git a/Justfile b/Justfile index 9a72c9b8..b5b95114 100644 --- a/Justfile +++ b/Justfile @@ -111,17 +111,34 @@ make-ui: make-ui-windows: @just release-windows #!/usr/bin/env sh + set -e if [ -f "./target/x86_64-pc-windows-gnu/release/goosed.exe" ]; then \ - echo "Copying Windows binary and DLLs to ui/desktop/src/bin..."; \ - mkdir -p ./ui/desktop/src/bin; \ - cp -f ./target/x86_64-pc-windows-gnu/release/goosed.exe ./ui/desktop/src/bin/; \ - cp -f ./target/x86_64-pc-windows-gnu/release/*.dll ./ui/desktop/src/bin/; \ - echo "Building Windows package..."; \ - cd ui/desktop && \ - npm run bundle:windows && \ - mkdir -p out/Goose-win32-x64/resources/bin && \ - cp -f src/bin/goosed.exe out/Goose-win32-x64/resources/bin/ && \ - cp -f src/bin/*.dll out/Goose-win32-x64/resources/bin/; \ + echo "Cleaning destination directory..." && \ + rm -rf ./ui/desktop/src/bin && \ + mkdir -p ./ui/desktop/src/bin && \ + echo "Copying Windows binary and DLLs..." && \ + cp -f ./target/x86_64-pc-windows-gnu/release/goosed.exe ./ui/desktop/src/bin/ && \ + cp -f ./target/x86_64-pc-windows-gnu/release/*.dll ./ui/desktop/src/bin/ && \ + if [ -d "./ui/desktop/src/platform/windows/bin" ]; then \ + echo "Copying Windows platform files..." && \ + for file in ./ui/desktop/src/platform/windows/bin/*.{exe,dll,cmd}; do \ + if [ -f "$file" ] && [ "$(basename "$file")" != "goosed.exe" ]; then \ + cp -f "$file" ./ui/desktop/src/bin/; \ + fi; \ + done && \ + if [ -d "./ui/desktop/src/platform/windows/bin/goose-npm" ]; then \ + echo "Setting up npm environment..." && \ + rsync -a --delete ./ui/desktop/src/platform/windows/bin/goose-npm/ ./ui/desktop/src/bin/goose-npm/; \ + fi && \ + echo "Windows-specific files copied successfully"; \ + fi && \ + echo "Starting Windows package build..." && \ + (cd ui/desktop && echo "In desktop directory, running npm bundle:windows..." && npm run bundle:windows) && \ + echo "Creating resources directory..." && \ + (cd ui/desktop && mkdir -p out/Goose-win32-x64/resources/bin) && \ + echo "Copying final binaries..." && \ + (cd ui/desktop && rsync -av src/bin/ out/Goose-win32-x64/resources/bin/) && \ + echo "Windows package build complete!"; \ else \ echo "Windows binary not found."; \ exit 1; \ diff --git a/crates/goose-server/src/routes/extension.rs b/crates/goose-server/src/routes/extension.rs index 07e1f355..056af4f0 100644 --- a/crates/goose-server/src/routes/extension.rs +++ b/crates/goose-server/src/routes/extension.rs @@ -108,6 +108,60 @@ async fn add_extension( return Err(StatusCode::UNAUTHORIZED); } + // If this is a Stdio extension that uses npx, check for Node.js installation + #[cfg(target_os = "windows")] + if let ExtensionConfigRequest::Stdio { cmd, .. } = &request { + if cmd.ends_with("npx.cmd") || cmd.ends_with("npx") { + // Check if Node.js is installed in standard locations + let node_exists = std::path::Path::new(r"C:\Program Files\nodejs\node.exe").exists() + || std::path::Path::new(r"C:\Program Files (x86)\nodejs\node.exe").exists(); + + if !node_exists { + // Get the directory containing npx.cmd + let cmd_path = std::path::Path::new(&cmd); + let script_dir = cmd_path.parent().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; + + // Run the Node.js installer script + let install_script = script_dir.join("install-node.cmd"); + + if install_script.exists() { + eprintln!("Installing Node.js..."); + let output = std::process::Command::new(&install_script) + .arg("https://nodejs.org/dist/v23.10.0/node-v23.10.0-x64.msi") + .output() + .map_err(|e| { + eprintln!("Failed to run Node.js installer: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + if !output.status.success() { + eprintln!( + "Failed to install Node.js: {}", + String::from_utf8_lossy(&output.stderr) + ); + return Ok(Json(ExtensionResponse { + error: true, + message: Some(format!( + "Failed to install Node.js: {}", + String::from_utf8_lossy(&output.stderr) + )), + })); + } + eprintln!("Node.js installation completed"); + } else { + eprintln!( + "Node.js installer script not found at: {}", + install_script.display() + ); + return Ok(Json(ExtensionResponse { + error: true, + message: Some("Node.js installer script not found".to_string()), + })); + } + } + } + } + // Load the configuration let config = Config::global(); @@ -419,20 +473,41 @@ fn is_command_allowed_with_allowlist( // Check against the allowlist Some(extensions) => { - // Strip out the Goose.app/Contents/Resources/bin/ prefix if present + // Strip out the Goose app resources/bin prefix if present (handle both macOS and Windows paths) let mut cmd_to_check = cmd.to_string(); + let mut is_goose_path = false; - // Check if the command path contains Goose.app/Contents/Resources/bin/ + // Check for macOS-style Goose.app path if cmd_to_check.contains("Goose.app/Contents/Resources/bin/") { - // Find the position of "Goose.app/Contents/Resources/bin/" if let Some(idx) = cmd_to_check.find("Goose.app/Contents/Resources/bin/") { - // Extract only the part after "Goose.app/Contents/Resources/bin/" cmd_to_check = cmd_to_check [(idx + "Goose.app/Contents/Resources/bin/".len())..] .to_string(); + is_goose_path = true; } - } else { - // Only apply the path check if we're not dealing with a Goose.app path + } + // Check for Windows-style Goose path with resources\bin + else if cmd_to_check.to_lowercase().contains("\\resources\\bin\\") + || cmd_to_check.contains("/resources/bin/") + { + // Also handle forward slashes + if let Some(idx) = cmd_to_check + .to_lowercase() + .rfind("\\resources\\bin\\") + .or_else(|| cmd_to_check.rfind("/resources/bin/")) + { + let path_len = if cmd_to_check.contains("/resources/bin/") { + "/resources/bin/".len() + } else { + "\\resources\\bin\\".len() + }; + cmd_to_check = cmd_to_check[(idx + path_len)..].to_string(); + is_goose_path = true; + } + } + + // Only check current directory for non-Goose paths + if !is_goose_path { // Check that the command exists as a peer command to current executable directory // Only apply this check if the command includes a path separator let current_exe = std::env::current_exe().unwrap(); @@ -817,6 +892,79 @@ mod tests { )); } + #[test] + fn test_windows_paths() { + let allowlist = create_test_allowlist(&["uvx mcp_snowflake", "uvx mcp_test"]); + + // Test various Windows path formats + let test_paths = vec![ + // Standard Windows path + r"C:\Users\MaxNovich\Downloads\Goose-1.0.17\resources\bin\uvx.exe", + // Path with different casing + r"C:\Users\MaxNovich\Downloads\Goose-1.0.17\Resources\Bin\uvx.exe", + // Path with forward slashes + r"C:/Users/MaxNovich/Downloads/Goose-1.0.17/resources/bin/uvx.exe", + // Path with spaces + r"C:\Program Files\Goose 1.0.17\resources\bin\uvx.exe", + // Path with version numbers + r"C:\Users\MaxNovich\Downloads\Goose-1.0.17-block.202504072238-76ffe-win32-x64\Goose-1.0.17-block.202504072238-76ffe-win32-x64\resources\bin\uvx.exe", + ]; + + for path in test_paths { + // Test with @latest version + let cmd = format!("{} mcp_snowflake@latest", path); + assert!( + is_command_allowed_with_allowlist(&cmd, &allowlist), + "Failed for path: {}", + path + ); + + // Test with specific version + let cmd_version = format!("{} mcp_test@1.2.3", path); + assert!( + is_command_allowed_with_allowlist(&cmd_version, &allowlist), + "Failed for path with version: {}", + path + ); + } + + // Test invalid paths that should be rejected + let invalid_paths = vec![ + // Path without resources\bin + r"C:\Users\MaxNovich\Downloads\uvx.exe", + // Path with modified resources\bin + r"C:\Users\MaxNovich\Downloads\Goose-1.0.17\resources_modified\bin\uvx.exe", + // Path with extra components + r"C:\Users\MaxNovich\Downloads\Goose-1.0.17\resources\bin\extra\uvx.exe", + ]; + + for path in invalid_paths { + let cmd = format!("{} mcp_snowflake@latest", path); + assert!( + !is_command_allowed_with_allowlist(&cmd, &allowlist), + "Should have rejected path: {}", + path + ); + } + } + + #[test] + fn test_windows_uvx_path() { + let allowlist = create_test_allowlist(&["uvx mcp_snowflake"]); + + // Test Windows-style path with uvx.exe + let windows_path = r"C:\Users\MaxNovich\Downloads\Goose-1.0.17-block.202504072238-76ffe-win32-x64\Goose-1.0.17-block.202504072238-76ffe-win32-x64\resources\bin\uvx.exe"; + let cmd = format!("{} mcp_snowflake@latest", windows_path); + + // This should be allowed because it's a valid uvx command in the Goose resources/bin directory + assert!(is_command_allowed_with_allowlist(&cmd, &allowlist)); + + // Test with different casing and backslashes + let windows_path_alt = r"c:\Users\MaxNovich\Downloads\Goose-1.0.17-block.202504072238-76ffe-win32-x64\Goose-1.0.17-block.202504072238-76ffe-win32-x64\Resources\Bin\uvx.exe"; + let cmd_alt = format!("{} mcp_snowflake@latest", windows_path_alt); + assert!(is_command_allowed_with_allowlist(&cmd_alt, &allowlist)); + } + #[test] fn test_fetch_allowed_extensions_from_url() { // Start a mock server - we need to use a blocking approach since fetch_allowed_extensions is blocking diff --git a/ui/desktop/.gitignore b/ui/desktop/.gitignore index 696aa48a..897652a0 100644 --- a/ui/desktop/.gitignore +++ b/ui/desktop/.gitignore @@ -2,6 +2,8 @@ node_modules .vite/ out src/bin/goosed -/src/bin/goosed.exe +src/bin/goose-npm/ +/src/bin/*.exe +/src/bin/*.cmd /playwright-report/ /test-results/ diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 0cf0a291..b9605706 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -12,7 +12,7 @@ "package": "electron-forge package", "make": "electron-forge make", "bundle:default": "npm run make && cd out/Goose-darwin-arm64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose.zip", - "bundle:windows": "node scripts/build-main.js && npm run make -- --platform=win32 --arch=x64 && node scripts/copy-windows-dlls.js", + "bundle:windows": "node scripts/build-main.js && node scripts/prepare-platform.js && npm run make -- --platform=win32 --arch=x64 && node scripts/copy-windows-dlls.js", "bundle:intel": "npm run make -- --arch=x64 && cd out/Goose-darwin-x64 && ditto -c -k --sequesterRsrc --keepParent Goose.app Goose_intel_mac.zip", "debug": "echo 'run --remote-debugging-port=8315' && lldb out/Goose-darwin-arm64/Goose.app", "test-e2e": "npm run generate-api && playwright test", diff --git a/ui/desktop/scripts/copy-windows-dlls.js b/ui/desktop/scripts/copy-windows-dlls.js index 369d303e..d95a6bfd 100644 --- a/ui/desktop/scripts/copy-windows-dlls.js +++ b/ui/desktop/scripts/copy-windows-dlls.js @@ -1,84 +1,49 @@ const fs = require('fs'); const path = require('path'); -const { execSync } = require('child_process'); -// Required DLLs that must be present -const REQUIRED_DLLS = [ - 'libstdc++-6.dll', - 'libgcc_s_seh-1.dll', - 'libwinpthread-1.dll' -]; +// Paths +const platformWinDir = path.join(__dirname, '..', 'src', 'platform', 'windows', 'bin'); +const outDir = path.join(__dirname, '..', 'out', 'Goose-win32-x64', 'resources', 'bin'); +const srcBinDir = path.join(__dirname, '..', 'src', 'bin'); -// Source and target directories -const sourceDir = path.join(__dirname, '../src/bin'); -const targetDir = path.join(__dirname, '../out/Goose-win32-x64/resources/bin'); - -function ensureDirectoryExists(dir) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - console.log(`Created directory: ${dir}`); - } -} - -function copyDLLs() { +// Helper function to copy files +function copyFiles(sourceDir, targetDir) { // Ensure target directory exists - ensureDirectoryExists(targetDir); - - // Get list of DLLs in source directory - const sourceDLLs = fs.readdirSync(sourceDir) - .filter(file => file.toLowerCase().endsWith('.dll')); - - console.log('Found DLLs in source directory:', sourceDLLs); - - // Check for missing required DLLs - const missingDLLs = REQUIRED_DLLS.filter(dll => - !sourceDLLs.includes(dll) - ); - - if (missingDLLs.length > 0) { - console.error('Missing required DLLs:', missingDLLs); - process.exit(1); + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, { recursive: true }); } - // Copy all DLLs and the executable to target directory - sourceDLLs.forEach(dll => { - const sourcePath = path.join(sourceDir, dll); - const targetPath = path.join(targetDir, dll); + // Copy all files from source to target directory + console.log(`Copying files to ${targetDir}...`); + fs.readdirSync(sourceDir).forEach(file => { + const srcPath = path.join(sourceDir, file); + const destPath = path.join(targetDir, file); - try { - fs.copyFileSync(sourcePath, targetPath); - console.log(`Copied ${dll} to ${targetDir}`); - } catch (err) { - console.error(`Error copying ${dll}:`, err); - process.exit(1); + // Skip .gitignore and README.md + if (file === '.gitignore' || file === 'README.md') { + return; } + + // Handle directories (like goose-npm) + if (fs.statSync(srcPath).isDirectory()) { + fs.cpSync(srcPath, destPath, { recursive: true, force: true }); + console.log(`Copied directory: ${file}`); + return; + } + + // Copy individual file + fs.copyFileSync(srcPath, destPath); + console.log(`Copied: ${file}`); }); - - // Copy the executable - const exeName = 'goosed.exe'; - const sourceExe = path.join(sourceDir, exeName); - const targetExe = path.join(targetDir, exeName); - - try { - if (fs.existsSync(sourceExe)) { - fs.copyFileSync(sourceExe, targetExe); - console.log(`Copied ${exeName} to ${targetDir}`); - } else { - console.error(`${exeName} not found in source directory`); - process.exit(1); - } - } catch (err) { - console.error(`Error copying ${exeName}:`, err); - process.exit(1); - } - - console.log('All files copied successfully'); } -// Main execution -try { - copyDLLs(); -} catch (err) { - console.error('Error during copy process:', err); - process.exit(1); -} \ No newline at end of file +// Copy to both directories +console.log('Copying Windows-specific files...'); + +// Copy to src/bin for development +copyFiles(platformWinDir, srcBinDir); + +// Copy to output directory for distribution +copyFiles(platformWinDir, outDir); + +console.log('Windows-specific files copied successfully'); diff --git a/ui/desktop/scripts/prepare-platform.js b/ui/desktop/scripts/prepare-platform.js new file mode 100644 index 00000000..3a06d21c --- /dev/null +++ b/ui/desktop/scripts/prepare-platform.js @@ -0,0 +1,16 @@ +const { execSync } = require('child_process'); +const path = require('path'); + +try { + if (process.platform === 'win32') { + execSync(path.join(__dirname, 'prepare-windows-npm.bat'), { stdio: 'inherit' }); + } else { + execSync(path.join(__dirname, 'prepare-windows-npm.sh'), { + stdio: 'inherit', + shell: '/bin/bash' + }); + } +} catch (error) { + console.error('Error preparing platform:', error); + process.exit(1); +} \ No newline at end of file diff --git a/ui/desktop/scripts/prepare-windows-npm.bat b/ui/desktop/scripts/prepare-windows-npm.bat new file mode 100644 index 00000000..fe6f31ee --- /dev/null +++ b/ui/desktop/scripts/prepare-windows-npm.bat @@ -0,0 +1,101 @@ +@echo off +setlocal enabledelayedexpansion + +rem Script to prepare Windows npm bundle +set "SCRIPT_DIR=%~dp0" +set "PLATFORM_WIN_DIR=%SCRIPT_DIR%\..\src\platform\windows" +set "WIN_BIN_DIR=%PLATFORM_WIN_DIR%\bin" +set "DEST_BIN_DIR=%SCRIPT_DIR%\..\src\bin" + +echo Preparing Windows npm bundle... +echo SCRIPT_DIR: %SCRIPT_DIR% +echo PLATFORM_WIN_DIR: %PLATFORM_WIN_DIR% +echo WIN_BIN_DIR: %WIN_BIN_DIR% +echo DEST_BIN_DIR: %DEST_BIN_DIR% + +rem Ensure directories exist +if not exist "%WIN_BIN_DIR%" mkdir "%WIN_BIN_DIR%" + +rem Node.js version and paths +set "NODE_VERSION=23.10.0" +set "NODE_MSI_URL=https://nodejs.org/dist/v%NODE_VERSION%/node-v%NODE_VERSION%-x64.msi" + +rem Create Windows Node.js installer script +echo Creating install-node.cmd... +( +echo @echo off +echo setlocal enabledelayedexpansion +echo. +echo REM Check if Node.js is installed in Program Files +echo if exist "C:\Program Files\nodejs\node.exe" ^( +echo echo Node.js found in Program Files +echo set "NODE_EXE=C:\Program Files\nodejs\node.exe" +echo goto :found +echo ^) +echo. +echo REM Check if Node.js is installed in Program Files ^(x86^) +echo if exist "C:\Program Files ^(x86^)\nodejs\node.exe" ^( +echo echo Node.js found in Program Files ^(x86^) +echo set "NODE_EXE=C:\Program Files ^(x86^)\nodejs\node.exe" +echo goto :found +echo ^) +echo. +echo echo Node.js not found in standard locations, installing... +echo. +echo REM Download Node.js MSI installer +echo powershell -Command "^& {[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%%1' -OutFile '%%TEMP%%\node-setup.msi'}" +echo. +echo REM Install Node.js silently +echo msiexec /i "%%TEMP%%\node-setup.msi" /qn +echo. +echo REM Wait a bit for installation to complete +echo timeout /t 5 /nobreak +echo. +echo REM Clean up +echo del "%%TEMP%%\node-setup.msi" +echo. +echo REM Set path to installed Node.js +echo set "NODE_EXE=C:\Program Files\nodejs\node.exe" +echo. +echo :found +echo echo Using Node.js: %%NODE_EXE%% +echo exit /b 0 +) > "%WIN_BIN_DIR%\install-node.cmd" + +rem Create a modified npx.cmd that checks for system Node.js first +echo Creating npx.cmd... +( +echo @ECHO OFF +echo SETLOCAL EnableDelayedExpansion +echo. +echo SET "SCRIPT_DIR=%%~dp0" +echo. +echo REM Try to find Node.js in standard locations first +echo if exist "C:\Program Files\nodejs\npx.cmd" ^( +echo "C:\Program Files\nodejs\npx.cmd" %%* +echo exit /b %%errorlevel%% +echo ^) +echo. +echo if exist "C:\Program Files ^(x86^)\nodejs\npx.cmd" ^( +echo "C:\Program Files ^(x86^)\nodejs\npx.cmd" %%* +echo exit /b %%errorlevel%% +echo ^) +echo. +echo REM If Node.js not found, run installer +echo call "%%SCRIPT_DIR%%install-node.cmd" "%NODE_MSI_URL%" +echo if errorlevel 1 ^( +echo echo Failed to install Node.js +echo exit /b 1 +echo ^) +echo. +echo REM Try using the newly installed Node.js +echo if exist "C:\Program Files\nodejs\npx.cmd" ^( +echo "C:\Program Files\nodejs\npx.cmd" %%* +echo exit /b %%errorlevel%% +echo ^) +echo. +echo echo Failed to find npx after Node.js installation +echo exit /b 1 +) > "%WIN_BIN_DIR%\npx.cmd" + +echo Windows npm bundle prepared successfully \ No newline at end of file diff --git a/ui/desktop/scripts/prepare-windows-npm.sh b/ui/desktop/scripts/prepare-windows-npm.sh new file mode 100755 index 00000000..1bddc59a --- /dev/null +++ b/ui/desktop/scripts/prepare-windows-npm.sh @@ -0,0 +1,101 @@ +#!/bin/bash +set -e + +# Script to prepare Windows npm bundle +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLATFORM_WIN_DIR="$SCRIPT_DIR/../src/platform/windows" +WIN_BIN_DIR="$PLATFORM_WIN_DIR/bin" +DEST_BIN_DIR="$SCRIPT_DIR/../src/bin" + +echo "Preparing Windows npm bundle..." +echo "SCRIPT_DIR: $SCRIPT_DIR" +echo "PLATFORM_WIN_DIR: $PLATFORM_WIN_DIR" +echo "WIN_BIN_DIR: $WIN_BIN_DIR" +echo "DEST_BIN_DIR: $DEST_BIN_DIR" + +# Ensure directories exist +mkdir -p "$WIN_BIN_DIR" + +# Node.js version and paths +NODE_VERSION="23.10.0" +NODE_MSI_URL="https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-x64.msi" + +# Create Windows Node.js installer script +echo "Creating install-node.cmd..." +cat > "$WIN_BIN_DIR/install-node.cmd" << 'EOL' +@echo off +setlocal enabledelayedexpansion + +REM Check if Node.js is installed in Program Files +if exist "C:\Program Files\nodejs\node.exe" ( + echo Node.js found in Program Files + set "NODE_EXE=C:\Program Files\nodejs\node.exe" + goto :found +) + +REM Check if Node.js is installed in Program Files (x86) +if exist "C:\Program Files (x86)\nodejs\node.exe" ( + echo Node.js found in Program Files (x86) + set "NODE_EXE=C:\Program Files (x86)\nodejs\node.exe" + goto :found +) + +echo Node.js not found in standard locations, installing... + +REM Download Node.js MSI installer +powershell -Command "& {[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%1' -OutFile '%TEMP%\node-setup.msi'}" + +REM Install Node.js silently +msiexec /i "%TEMP%\node-setup.msi" /qn + +REM Wait a bit for installation to complete +timeout /t 5 /nobreak + +REM Clean up +del "%TEMP%\node-setup.msi" + +REM Set path to installed Node.js +set "NODE_EXE=C:\Program Files\nodejs\node.exe" + +:found +echo Using Node.js: %NODE_EXE% +exit /b 0 +EOL + +# Create a modified npx.cmd that checks for system Node.js first +echo "Creating npx.cmd..." +cat > "$WIN_BIN_DIR/npx.cmd" << 'EOL' +@ECHO OFF +SETLOCAL EnableDelayedExpansion + +SET "SCRIPT_DIR=%~dp0" + +REM Try to find Node.js in standard locations first +if exist "C:\Program Files\nodejs\npx.cmd" ( + "C:\Program Files\nodejs\npx.cmd" %* + exit /b %errorlevel% +) + +if exist "C:\Program Files (x86)\nodejs\npx.cmd" ( + "C:\Program Files (x86)\nodejs\npx.cmd" %* + exit /b %errorlevel% +) + +REM If Node.js not found, run installer +call "%SCRIPT_DIR%install-node.cmd" "https://nodejs.org/dist/v23.10.0/node-v23.10.0-x64.msi" +if errorlevel 1 ( + echo Failed to install Node.js + exit /b 1 +) + +REM Try using the newly installed Node.js +if exist "C:\Program Files\nodejs\npx.cmd" ( + "C:\Program Files\nodejs\npx.cmd" %* + exit /b %errorlevel% +) + +echo Failed to find npx after Node.js installation +exit /b 1 +EOL + +echo "Windows npm bundle prepared successfully" diff --git a/ui/desktop/src/bin/.gitkeep b/ui/desktop/src/bin/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/ui/desktop/src/bin/jbang b/ui/desktop/src/bin/jbang deleted file mode 100755 index 8748b44d..00000000 --- a/ui/desktop/src/bin/jbang +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -# Enable strict mode to exit on errors and unset variables -set -euo pipefail - -# Set log file -LOG_FILE="/tmp/mcp.log" - -# Clear the log file at the start -> "$LOG_FILE" - -# Function for logging -log() { - local MESSAGE="$1" - echo "$(date +'%Y-%m-%d %H:%M:%S') - $MESSAGE" | tee -a "$LOG_FILE" -} - -# Trap errors and log them before exiting -trap 'log "An error occurred. Exiting with status $?."' ERR - -log "Starting jbang setup script." - -# Ensure ~/.config/goose/mcp-hermit/bin exists -log "Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist." -mkdir -p ~/.config/goose/mcp-hermit/bin - -# Change to the ~/.config/goose/mcp-hermit directory -log "Changing to directory ~/.config/goose/mcp-hermit." -cd ~/.config/goose/mcp-hermit - -# Check if hermit binary exists and download if not -if [ ! -f ~/.config/goose/mcp-hermit/bin/hermit ]; then - log "Hermit binary not found. Downloading hermit binary." - curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \ - | gzip -dc > ~/.config/goose/mcp-hermit/bin/hermit && chmod +x ~/.config/goose/mcp-hermit/bin/hermit - log "Hermit binary downloaded and made executable." -else - log "Hermit binary already exists. Skipping download." -fi - -log "setting hermit cache to be local for MCP servers" -mkdir -p ~/.config/goose/mcp-hermit/cache -export HERMIT_STATE_DIR=~/.config/goose/mcp-hermit/cache - -# Update PATH -export PATH=~/.config/goose/mcp-hermit/bin:$PATH -log "Updated PATH to include ~/.config/goose/mcp-hermit/bin." - -# Initialize hermit -log "Initializing hermit." -hermit init >> "$LOG_FILE" - -# Install OpenJDK using hermit -log "Installing OpenJDK with hermit." -hermit install openjdk@17 >> "$LOG_FILE" - -# Download and install jbang if not present -if [ ! -f ~/.config/goose/mcp-hermit/bin/jbang ]; then - log "Downloading and installing jbang." - curl -Ls https://sh.jbang.dev | bash -s - app setup - cp ~/.jbang/bin/jbang ~/.config/goose/mcp-hermit/bin/ -fi - -# Verify installations -log "Verifying installation locations:" -log "hermit: $(which hermit)" -log "java: $(which java)" -log "jbang: $(which jbang)" - -# Check for custom registry settings -log "Checking for GOOSE_JBANG_REGISTRY environment variable for custom jbang registry setup..." -if [ -n "${GOOSE_JBANG_REGISTRY:-}" ] && curl -s --head --fail "$GOOSE_JBANG_REGISTRY" > /dev/null; then - log "Checking custom goose registry availability: $GOOSE_JBANG_REGISTRY" - log "$GOOSE_JBANG_REGISTRY is accessible. Setting it as JBANG_REPO." - export JBANG_REPO="$GOOSE_JBANG_REGISTRY" -else - log "GOOSE_JBANG_REGISTRY is not set or not accessible. Using default jbang repository." -fi - -# Trust all jbang scripts that a user might install. Without this, Jbang will attempt to -# prompt the user to trust each script. However, Goose does not surfact this modal and without -# user input, the addExtension method will timeout and fail. -jbang --quiet trust add * - -# Final step: Execute jbang with passed arguments, always including --fresh and --quiet -log "Executing 'jbang' command with arguments: $*" -jbang --fresh --quiet "$@" || log "Failed to execute 'jbang' with arguments: $*" - -log "jbang setup script completed successfully." \ No newline at end of file diff --git a/ui/desktop/src/bin/npx b/ui/desktop/src/bin/npx deleted file mode 100755 index e3faa03b..00000000 --- a/ui/desktop/src/bin/npx +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/bash - -# Enable strict mode to exit on errors and unset variables -set -euo pipefail - -# Set log file -LOG_FILE="/tmp/mcp.log" - -# Clear the log file at the start -> "$LOG_FILE" - -# Function for logging -log() { - local MESSAGE="$1" - echo "$(date +'%Y-%m-%d %H:%M:%S') - $MESSAGE" | tee -a "$LOG_FILE" -} - -# Trap errors and log them before exiting -trap 'log "An error occurred. Exiting with status $?."' ERR - -log "Starting npx setup script." - -# Ensure ~/.config/goose/mcp-hermit/bin exists -log "Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist." -mkdir -p ~/.config/goose/mcp-hermit/bin - -# Change to the ~/.config/goose/mcp-hermit directory -log "Changing to directory ~/.config/goose/mcp-hermit." -cd ~/.config/goose/mcp-hermit - - -# Check if hermit binary exists and download if not -if [ ! -f ~/.config/goose/mcp-hermit/bin/hermit ]; then - log "Hermit binary not found. Downloading hermit binary." - curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \ - | gzip -dc > ~/.config/goose/mcp-hermit/bin/hermit && chmod +x ~/.config/goose/mcp-hermit/bin/hermit - log "Hermit binary downloaded and made executable." -else - log "Hermit binary already exists. Skipping download." -fi - - -log "setting hermit cache to be local for MCP servers" -mkdir -p ~/.config/goose/mcp-hermit/cache -export HERMIT_STATE_DIR=~/.config/goose/mcp-hermit/cache - - -# Update PATH -export PATH=~/.config/goose/mcp-hermit/bin:$PATH -log "Updated PATH to include ~/.config/goose/mcp-hermit/bin." - - -# Verify hermit installation -log "Checking for hermit in PATH." -which hermit >> "$LOG_FILE" - -# Initialize hermit -log "Initializing hermit." -hermit init >> "$LOG_FILE" - -# Install Node.js using hermit -log "Installing Node.js with hermit." -hermit install node >> "$LOG_FILE" - -# Verify installations -log "Verifying installation locations:" -log "hermit: $(which hermit)" -log "node: $(which node)" -log "npx: $(which npx)" - - -log "Checking for GOOSE_NPM_REGISTRY and GOOSE_NPM_CERT environment variables for custom npm registry setup..." -# Check if GOOSE_NPM_REGISTRY is set and accessible -if [ -n "${GOOSE_NPM_REGISTRY:-}" ] && curl -s --head --fail "$GOOSE_NPM_REGISTRY" > /dev/null; then - log "Checking custom goose registry availability: $GOOSE_NPM_REGISTRY" - log "$GOOSE_NPM_REGISTRY is accessible. Using it for npm registry." - export NPM_CONFIG_REGISTRY="$GOOSE_NPM_REGISTRY" - - # Check if GOOSE_NPM_CERT is set and accessible - if [ -n "${GOOSE_NPM_CERT:-}" ] && curl -s --head --fail "$GOOSE_NPM_CERT" > /dev/null; then - log "Downloading certificate from: $GOOSE_NPM_CERT" - curl -sSL -o ~/.config/goose/mcp-hermit/cert.pem "$GOOSE_NPM_CERT" - if [ $? -eq 0 ]; then - log "Certificate downloaded successfully." - export NODE_EXTRA_CA_CERTS=~/.config/goose/mcp-hermit/cert.pem - else - log "Unable to download the certificate. Skipping certificate setup." - fi - else - log "GOOSE_NPM_CERT is either not set or not accessible. Skipping certificate setup." - fi - -else - log "GOOSE_NPM_REGISTRY is either not set or not accessible. Falling back to default npm registry." - export NPM_CONFIG_REGISTRY="https://registry.npmjs.org/" -fi - - - - -# Final step: Execute npx with passed arguments -log "Executing 'npx' command with arguments: $*" -npx "$@" || log "Failed to execute 'npx' with arguments: $*" - -log "npx setup script completed successfully." \ No newline at end of file diff --git a/ui/desktop/src/bin/npx.cmd b/ui/desktop/src/bin/npx.cmd deleted file mode 100644 index ab991abf..00000000 --- a/ui/desktop/src/bin/npx.cmd +++ /dev/null @@ -1,20 +0,0 @@ -:: Created by npm, please don't edit manually. -@ECHO OFF - -SETLOCAL - -SET "NODE_EXE=%~dp0\node.exe" -IF NOT EXIST "%NODE_EXE%" ( - SET "NODE_EXE=node" -) - -SET "NPM_PREFIX_JS=%~dp0\node_modules\npm\bin\npm-prefix.js" -SET "NPX_CLI_JS=%~dp0\node_modules\npm\bin\npx-cli.js" -FOR /F "delims=" %%F IN ('CALL "%NODE_EXE%" "%NPM_PREFIX_JS%"') DO ( - SET "NPM_PREFIX_NPX_CLI_JS=%%F\node_modules\npm\bin\npx-cli.js" -) -IF EXIST "%NPM_PREFIX_NPX_CLI_JS%" ( - SET "NPX_CLI_JS=%NPM_PREFIX_NPX_CLI_JS%" -) - -"%NODE_EXE%" "%NPX_CLI_JS%" %* diff --git a/ui/desktop/src/bin/uvx b/ui/desktop/src/bin/uvx deleted file mode 100755 index 8a1eec12..00000000 --- a/ui/desktop/src/bin/uvx +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -# Enable strict mode to exit on errors and unset variables -set -euo pipefail - -# Set log file -LOG_FILE="/tmp/mcp.log" - -# Clear the log file at the start -> "$LOG_FILE" - -# Function for logging -log() { - local MESSAGE="$1" - echo "$(date +'%Y-%m-%d %H:%M:%S') - $MESSAGE" | tee -a "$LOG_FILE" -} - -# Trap errors and log them before exiting -trap 'log "An error occurred. Exiting with status $?."' ERR - -log "Starting uvx setup script." - -# Ensure ~/.config/goose/mcp-hermit/bin exists -log "Creating directory ~/.config/goose/mcp-hermit/bin if it does not exist." -mkdir -p ~/.config/goose/mcp-hermit/bin - -# Change to the ~/.config/goose/mcp-hermit directory -log "Changing to directory ~/.config/goose/mcp-hermit." -cd ~/.config/goose/mcp-hermit - -# Check if hermit binary exists and download if not -if [ ! -f ~/.config/goose/mcp-hermit/bin/hermit ]; then - log "Hermit binary not found. Downloading hermit binary." - curl -fsSL "https://github.com/cashapp/hermit/releases/download/stable/hermit-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/').gz" \ - | gzip -dc > ~/.config/goose/mcp-hermit/bin/hermit && chmod +x ~/.config/goose/mcp-hermit/bin/hermit - log "Hermit binary downloaded and made executable." -else - log "Hermit binary already exists. Skipping download." -fi - - -log "setting hermit cache to be local for MCP servers" -mkdir -p ~/.config/goose/mcp-hermit/cache -export HERMIT_STATE_DIR=~/.config/goose/mcp-hermit/cache - -# Update PATH -export PATH=~/.config/goose/mcp-hermit/bin:$PATH -log "Updated PATH to include ~/.config/goose/mcp-hermit/bin." - - -# Verify hermit installation -log "Checking for hermit in PATH." -which hermit >> "$LOG_FILE" - -# Initialize hermit -log "Initializing hermit." -hermit init >> "$LOG_FILE" - -# Initialize python >= 3.10 -log "hermit install python 3.10" -hermit install python3@3.10 >> "$LOG_FILE" - -# Install UV for python using hermit -log "Installing UV with hermit." -hermit install uv >> "$LOG_FILE" - -# Verify installations -log "Verifying installation locations:" -log "hermit: $(which hermit)" -log "uv: $(which uv)" -log "uvx: $(which uvx)" - - -log "Checking for GOOSE_UV_REGISTRY environment variable for custom python/pip/UV registry setup..." -# Check if GOOSE_UV_REGISTRY is set and accessible -if [ -n "${GOOSE_UV_REGISTRY:-}" ] && curl -s --head --fail "$GOOSE_UV_REGISTRY" > /dev/null; then - log "Checking custom goose registry availability: $GOOSE_UV_REGISTRY" - log "$GOOSE_UV_REGISTRY is accessible, setting it as UV_INDEX_URL. Setting UV_NATIVE_TLS to true." - export UV_INDEX_URL="$GOOSE_UV_REGISTRY" - export UV_NATIVE_TLS=true -else - log "Neither GOOSE_UV_REGISTRY nor UV_INDEX_URL is set. Falling back to default configuration." -fi - -# Final step: Execute uvx with passed arguments -log "Executing 'uvx' command with arguments: $*" -uvx "$@" || log "Failed to execute 'uvx' with arguments: $*" - -log "uvx setup script completed successfully." diff --git a/ui/desktop/src/platform/windows/bin/README.md b/ui/desktop/src/platform/windows/bin/README.md new file mode 100644 index 00000000..c385bba4 --- /dev/null +++ b/ui/desktop/src/platform/windows/bin/README.md @@ -0,0 +1,21 @@ +# Windows-Specific Binaries + +This directory contains Windows-specific binaries and scripts that are only included during Windows builds. + +## Components + +### Node.js Installation +- `install-node.cmd` - Script to check for and install Node.js if needed +- `npx.cmd` - Wrapper script that ensures Node.js is installed and uses system npx + +### Windows Binaries +- `*.dll` files - Required Windows dynamic libraries +- `*.exe` files - Windows executables + +## Build Process + +These files are generated during the Windows build process by: +1. `prepare-windows-npm.sh` - Creates Node.js installation scripts +2. `copy-windows-dlls.js` - Copies all Windows-specific files to the output directory + +None of these files should be committed to the repository - they are generated fresh during each Windows build. diff --git a/ui/desktop/src/platform/windows/bin/install-node.cmd b/ui/desktop/src/platform/windows/bin/install-node.cmd new file mode 100644 index 00000000..84fbb41d --- /dev/null +++ b/ui/desktop/src/platform/windows/bin/install-node.cmd @@ -0,0 +1,37 @@ +@echo off +setlocal enabledelayedexpansion + +REM Check if Node.js is installed in Program Files +if exist "C:\Program Files\nodejs\node.exe" ( + echo Node.js found in Program Files + set "NODE_EXE=C:\Program Files\nodejs\node.exe" + goto :found +) + +REM Check if Node.js is installed in Program Files (x86) +if exist "C:\Program Files (x86)\nodejs\node.exe" ( + echo Node.js found in Program Files (x86) + set "NODE_EXE=C:\Program Files (x86)\nodejs\node.exe" + goto :found +) + +echo Node.js not found in standard locations, installing... + +REM Download Node.js MSI installer +powershell -Command "& {[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '%1' -OutFile '%TEMP%\node-setup.msi'}" + +REM Install Node.js silently +msiexec /i "%TEMP%\node-setup.msi" /qn + +REM Wait a bit for installation to complete +timeout /t 5 /nobreak + +REM Clean up +del "%TEMP%\node-setup.msi" + +REM Set path to installed Node.js +set "NODE_EXE=C:\Program Files\nodejs\node.exe" + +:found +echo Using Node.js: %NODE_EXE% +exit /b 0 diff --git a/ui/desktop/src/platform/windows/bin/jbang.cmd b/ui/desktop/src/platform/windows/bin/jbang.cmd new file mode 100644 index 00000000..ec505a8f --- /dev/null +++ b/ui/desktop/src/platform/windows/bin/jbang.cmd @@ -0,0 +1,2 @@ +@echo off +"%~dp0\jbang" %* diff --git a/ui/desktop/src/platform/windows/bin/libgcc_s_seh-1.dll b/ui/desktop/src/platform/windows/bin/libgcc_s_seh-1.dll new file mode 100755 index 00000000..2a858d4f Binary files /dev/null and b/ui/desktop/src/platform/windows/bin/libgcc_s_seh-1.dll differ diff --git a/ui/desktop/src/platform/windows/bin/libstdc++-6.dll b/ui/desktop/src/platform/windows/bin/libstdc++-6.dll new file mode 100755 index 00000000..e6c135f7 Binary files /dev/null and b/ui/desktop/src/platform/windows/bin/libstdc++-6.dll differ diff --git a/ui/desktop/src/platform/windows/bin/libwinpthread-1.dll b/ui/desktop/src/platform/windows/bin/libwinpthread-1.dll new file mode 100755 index 00000000..d87e5318 Binary files /dev/null and b/ui/desktop/src/platform/windows/bin/libwinpthread-1.dll differ diff --git a/ui/desktop/src/platform/windows/bin/npx.cmd b/ui/desktop/src/platform/windows/bin/npx.cmd new file mode 100644 index 00000000..7c019e5f --- /dev/null +++ b/ui/desktop/src/platform/windows/bin/npx.cmd @@ -0,0 +1,31 @@ +@ECHO OFF +SETLOCAL EnableDelayedExpansion + +SET "SCRIPT_DIR=%~dp0" + +REM Try to find Node.js in standard locations first +if exist "C:\Program Files\nodejs\npx.cmd" ( + "C:\Program Files\nodejs\npx.cmd" %* + exit /b %errorlevel% +) + +if exist "C:\Program Files (x86)\nodejs\npx.cmd" ( + "C:\Program Files (x86)\nodejs\npx.cmd" %* + exit /b %errorlevel% +) + +REM If Node.js not found, run installer +call "%SCRIPT_DIR%install-node.cmd" "https://nodejs.org/dist/v23.10.0/node-v23.10.0-x64.msi" +if errorlevel 1 ( + echo Failed to install Node.js + exit /b 1 +) + +REM Try using the newly installed Node.js +if exist "C:\Program Files\nodejs\npx.cmd" ( + "C:\Program Files\nodejs\npx.cmd" %* + exit /b %errorlevel% +) + +echo Failed to find npx after Node.js installation +exit /b 1 diff --git a/ui/desktop/src/bin/uv.exe b/ui/desktop/src/platform/windows/bin/uv.exe similarity index 100% rename from ui/desktop/src/bin/uv.exe rename to ui/desktop/src/platform/windows/bin/uv.exe diff --git a/ui/desktop/src/bin/uvx.exe b/ui/desktop/src/platform/windows/bin/uvx.exe similarity index 100% rename from ui/desktop/src/bin/uvx.exe rename to ui/desktop/src/platform/windows/bin/uvx.exe diff --git a/ui/desktop/src/utils/binaryPath.ts b/ui/desktop/src/utils/binaryPath.ts index caec53b5..049e6550 100644 --- a/ui/desktop/src/utils/binaryPath.ts +++ b/ui/desktop/src/utils/binaryPath.ts @@ -4,18 +4,41 @@ import Electron from 'electron'; import log from './logger'; export const getBinaryPath = (app: Electron.App, binaryName: string): string => { - const isDev = process.env.NODE_ENV === 'development'; - const isPackaged = app.isPackaged; const isWindows = process.platform === 'win32'; - const executableName = isWindows - ? binaryName === 'npx' - ? 'npx.cmd' - : `${binaryName}.exe` - : binaryName; - const possiblePaths = []; + if (isWindows) { + addPaths(isWindows, possiblePaths, `${binaryName}.exe`, app); + addPaths(isWindows, possiblePaths, `${binaryName}.cmd`, app); + } else { + addPaths(isWindows, possiblePaths, binaryName, app); + } + for (const binPath of possiblePaths) { + try { + if (fs.existsSync(binPath)) { + return binPath; + } + } catch (error) { + log.error(`Error checking path ${binPath}:`, error); + } + } + + throw new Error( + `Could not find ${binaryName} binary in any of the expected locations: ${possiblePaths.join( + ', ' + )}` + ); +}; + +const addPaths = ( + isWindows: boolean, + possiblePaths: any[], + executableName: string, + app: Electron.App +): void => { + const isDev = process.env.NODE_ENV === 'development'; + const isPackaged = app.isPackaged; if (isDev && !isPackaged) { possiblePaths.push( path.join(process.cwd(), 'src', 'bin', executableName), @@ -36,18 +59,4 @@ export const getBinaryPath = (app: Electron.App, binaryName: string): string => ); } } - - for (const binPath of possiblePaths) { - try { - if (fs.existsSync(binPath)) { - return binPath; - } - } catch (error) { - log.error(`Error checking path ${binPath}:`, error); - } - } - - throw new Error( - `Could not find ${binaryName} binary in any of the expected locations: ${possiblePaths.join(', ')}` - ); };