fix windows extensions (#1968)

This commit is contained in:
Max Novich
2025-04-08 10:07:37 -07:00
committed by GitHub
parent 48ac6a3925
commit 03c06c9bf6
23 changed files with 562 additions and 415 deletions

View File

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

View File

@@ -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;
}
}
// 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 {
// Only apply the path check if we're not dealing with a Goose.app path
"\\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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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%" %*

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
@echo off
"%~dp0\jbang" %*

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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