#!/usr/bin/env node
/**
* Mac PKG Installer Builder with Component Selection
*
* Creates a multi-component installer matching Windows NSIS structure:
* 1. MCP Server (Required) - Core server + Claude config
* 2. Foundry Module (Optional, default ON) - Foundry VTT module
* 3. ComfyUI (Optional, default OFF) - AI map generation with models
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const VERSION = process.env.VERSION || require('../package.json').version;
// Paths
const ROOT_DIR = path.join(__dirname, '..');
const BUILD_DIR = path.join(__dirname, 'build');
const MAC_DIR = path.join(__dirname, 'mac');
// Component build directories
const CORE_ROOT = path.join(BUILD_DIR, 'core-pkg-root');
const FOUNDRY_ROOT = path.join(BUILD_DIR, 'foundry-pkg-root');
const COMFYUI_ROOT = path.join(BUILD_DIR, 'comfyui-pkg-root');
// Output packages
const CORE_PKG = path.join(BUILD_DIR, 'FoundryMCP-Core.pkg');
const FOUNDRY_PKG = path.join(BUILD_DIR, 'FoundryMCP-FoundryModule.pkg');
const COMFYUI_PKG = path.join(BUILD_DIR, 'FoundryMCP-ComfyUI.pkg');
const FINAL_PKG = path.join(BUILD_DIR, `FoundryMCPServer-${VERSION}-macOS.pkg`);
console.log('π Building Multi-Component Mac PKG Installer');
console.log(`Version: ${VERSION}`);
console.log('');
console.log('Components:');
console.log(' 1. MCP Server (Required) - ~5MB');
console.log(' 2. Foundry Module (Optional, Default ON) - ~5MB');
console.log(' 3. ComfyUI AI Maps (Optional, Default ON) - ~13GB download');
console.log('');
// Helper to copy recursively
function copyRecursive(src, dest) {
if (!fs.existsSync(src)) return;
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
fs.readdirSync(src).forEach(item => {
copyRecursive(path.join(src, item), path.join(dest, item));
});
} else {
fs.copyFileSync(src, dest);
}
}
// Clean build directory
console.log('π Cleaning build directory...');
[CORE_ROOT, FOUNDRY_ROOT, COMFYUI_ROOT].forEach(dir => {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
// ============================================================================
// COMPONENT 1: Core MCP Server (Required)
// ============================================================================
console.log('\nπ¦ Building Component 1: MCP Server (Required)...');
const coreAppBundle = path.join(CORE_ROOT, 'Applications', 'FoundryMCPServer.app', 'Contents', 'Resources');
fs.mkdirSync(coreAppBundle, { recursive: true });
// Copy MCP server bundles
const mcpServerDist = path.join(ROOT_DIR, 'packages', 'mcp-server', 'dist');
const mcpServerDest = path.join(coreAppBundle, 'foundry-mcp-server');
fs.mkdirSync(mcpServerDest, { recursive: true });
if (!fs.existsSync(path.join(mcpServerDist, 'backend.bundle.cjs'))) {
console.error('β backend.bundle.cjs not found. Run: npm run build:bundle');
process.exit(1);
}
fs.copyFileSync(
path.join(mcpServerDist, 'backend.bundle.cjs'),
path.join(mcpServerDest, 'backend.bundle.cjs')
);
fs.copyFileSync(
path.join(mcpServerDist, 'index.bundle.cjs'),
path.join(mcpServerDest, 'index.cjs')
);
fs.copyFileSync(
path.join(ROOT_DIR, 'packages', 'mcp-server', 'package.json'),
path.join(mcpServerDest, 'package.json')
);
console.log('β
MCP Server files copied');
// Create Core postinstall script (configures Claude Desktop)
const coreScripts = path.join(BUILD_DIR, 'core-scripts');
fs.mkdirSync(coreScripts, { recursive: true });
const corePostinstall = `#!/bin/bash
# Core MCP Server postinstall - Configures Claude Desktop
set -e
CURRENT_USER=$(stat -f '%Su' /dev/console)
USER_HOME=$(eval echo ~$CURRENT_USER)
echo "Configuring Claude Desktop for user: $CURRENT_USER"
CLAUDE_CONFIG_DIR="$USER_HOME/Library/Application Support/Claude"
CLAUDE_CONFIG="$CLAUDE_CONFIG_DIR/claude_desktop_config.json"
MCP_SERVER_DIR="$USER_HOME/Library/Application Support/FoundryMCPServer"
SERVER_PATH="/Applications/FoundryMCPServer.app/Contents/Resources/foundry-mcp-server/index.cjs"
# Create Claude config directory
mkdir -p "$CLAUDE_CONFIG_DIR"
chown "$CURRENT_USER:staff" "$CLAUDE_CONFIG_DIR"
# Create MCP Server directory for logs
mkdir -p "$MCP_SERVER_DIR"
chown "$CURRENT_USER:staff" "$MCP_SERVER_DIR"
# Backup existing config
if [ -f "$CLAUDE_CONFIG" ]; then
cp "$CLAUDE_CONFIG" "$CLAUDE_CONFIG.backup.$(date +%s)"
fi
# Write Claude config
cat > "$CLAUDE_CONFIG" <<EOF
{
"mcpServers": {
"foundry-mcp": {
"command": "node",
"args": [
"$SERVER_PATH"
],
"env": {
"FOUNDRY_HOST": "localhost",
"FOUNDRY_PORT": "31415"
}
}
}
}
EOF
chown "$CURRENT_USER:staff" "$CLAUDE_CONFIG"
chmod 644 "$CLAUDE_CONFIG"
echo "β
Claude Desktop configured"
exit 0
`;
fs.writeFileSync(path.join(coreScripts, 'postinstall'), corePostinstall);
fs.chmodSync(path.join(coreScripts, 'postinstall'), 0o755);
// Build core component package
execSync(`pkgbuild \\
--root "${CORE_ROOT}" \\
--scripts "${coreScripts}" \\
--identifier com.foundry-mcp.core \\
--version "${VERSION}" \\
--install-location / \\
"${CORE_PKG}"`, {
stdio: 'inherit'
});
console.log('β
Core component package built');
// ============================================================================
// COMPONENT 2: Foundry Module (Optional)
// ============================================================================
console.log('\nπ¦ Building Component 2: Foundry Module (Optional)...');
const foundryAppBundle = path.join(FOUNDRY_ROOT, 'Applications', 'FoundryMCPServer.app', 'Contents', 'Resources');
fs.mkdirSync(foundryAppBundle, { recursive: true });
// Copy Foundry module
const moduleSourceRoot = path.join(ROOT_DIR, 'packages', 'foundry-module');
const moduleDest = path.join(foundryAppBundle, 'foundry-module');
if (!fs.existsSync(path.join(moduleSourceRoot, 'dist'))) {
console.error('β Foundry module dist not found. Run: npm run build');
process.exit(1);
}
copyRecursive(path.join(moduleSourceRoot, 'dist'), path.join(moduleDest, 'dist'));
['lang', 'styles', 'templates'].forEach(folder => {
const srcPath = path.join(moduleSourceRoot, folder);
if (fs.existsSync(srcPath)) {
copyRecursive(srcPath, path.join(moduleDest, folder));
}
});
fs.copyFileSync(
path.join(moduleSourceRoot, 'module.json'),
path.join(moduleDest, 'module.json')
);
console.log('β
Foundry module files copied');
// Create Foundry postinstall script (installs module to Foundry)
const foundryScripts = path.join(BUILD_DIR, 'foundry-scripts');
fs.mkdirSync(foundryScripts, { recursive: true });
const foundryPostinstall = `#!/bin/bash
# Foundry Module postinstall - Installs module to Foundry VTT
set -e
CURRENT_USER=$(stat -f '%Su' /dev/console)
USER_HOME=$(eval echo ~$CURRENT_USER)
echo "Installing Foundry MCP Bridge module..."
MODULE_SOURCE="/Applications/FoundryMCPServer.app/Contents/Resources/foundry-module"
# Try to find Foundry VTT installation
FOUNDRY_PATHS=(
"$USER_HOME/Library/Application Support/FoundryVTT/Data/modules"
"$USER_HOME/FoundryVTT/Data/modules"
"/Applications/FoundryVTT/Data/modules"
)
for FOUNDRY_PATH in "\${FOUNDRY_PATHS[@]}"; do
if [ -d "$FOUNDRY_PATH" ]; then
MODULE_DEST="$FOUNDRY_PATH/foundry-mcp-bridge"
# Remove old version
if [ -d "$MODULE_DEST" ]; then
rm -rf "$MODULE_DEST"
fi
# Copy module
cp -R "$MODULE_SOURCE" "$MODULE_DEST"
chown -R "$CURRENT_USER:staff" "$MODULE_DEST"
echo "β
Foundry module installed to: $MODULE_DEST"
exit 0
fi
done
echo "βΉοΈ Foundry VTT not detected - module will be installed on first connection"
exit 0
`;
fs.writeFileSync(path.join(foundryScripts, 'postinstall'), foundryPostinstall);
fs.chmodSync(path.join(foundryScripts, 'postinstall'), 0o755);
// Build Foundry component package
execSync(`pkgbuild \\
--root "${FOUNDRY_ROOT}" \\
--scripts "${foundryScripts}" \\
--identifier com.foundry-mcp.foundry-module \\
--version "${VERSION}" \\
--install-location / \\
"${FOUNDRY_PKG}"`, {
stdio: 'inherit'
});
console.log('β
Foundry module component package built');
// ============================================================================
// COMPONENT 3: ComfyUI (Optional)
// ============================================================================
console.log('\nπ¦ Building Component 3: ComfyUI AI Maps (Optional)...');
const comfyuiAppBundle = path.join(COMFYUI_ROOT, 'Applications', 'FoundryMCPServer.app', 'Contents', 'Resources');
fs.mkdirSync(comfyuiAppBundle, { recursive: true });
// Copy ComfyUI setup script
const setupScript = path.join(MAC_DIR, 'setup-comfyui-headless.js');
if (!fs.existsSync(setupScript)) {
console.error('β setup-comfyui-headless.js not found');
process.exit(1);
}
fs.copyFileSync(setupScript, path.join(comfyuiAppBundle, 'setup-comfyui-headless.js'));
fs.chmodSync(path.join(comfyuiAppBundle, 'setup-comfyui-headless.js'), 0o755);
console.log('β
ComfyUI setup script copied');
// Create ComfyUI postinstall script (runs headless setup)
const comfyuiScripts = path.join(BUILD_DIR, 'comfyui-scripts');
fs.mkdirSync(comfyuiScripts, { recursive: true });
const comfyuiPostinstall = `#!/bin/bash
# ComfyUI postinstall - Downloads and installs ComfyUI + AI models
set -e
CURRENT_USER=$(stat -f '%Su' /dev/console)
USER_HOME=$(eval echo ~$CURRENT_USER)
echo ""
echo "ββββββββββββββββββββββββββββββββββββββββ"
echo "Installing ComfyUI AI Map Generation"
echo "ββββββββββββββββββββββββββββββββββββββββ"
echo ""
echo "This will download and install:"
echo " - Python 3.11 (~40MB)"
echo " - ComfyUI (~100MB)"
echo " - PyTorch with MPS support (~2GB)"
echo " - AI Models (~13GB)"
echo ""
echo "Total download: ~15GB"
echo "Installation time: 20-30 minutes"
echo ""
# Find node
NODE_PATH=""
if [ -f "/usr/local/bin/node" ]; then
NODE_PATH="/usr/local/bin/node"
elif [ -f "/opt/homebrew/bin/node" ]; then
NODE_PATH="/opt/homebrew/bin/node"
elif command -v node >/dev/null 2>&1; then
NODE_PATH=$(command -v node)
fi
if [ -z "$NODE_PATH" ]; then
echo "β Node.js not found. Please install from https://nodejs.org/"
exit 1
fi
SETUP_SCRIPT="/Applications/FoundryMCPServer.app/Contents/Resources/setup-comfyui-headless.js"
echo "Running ComfyUI setup..."
USER_HOME="$USER_HOME" CURRENT_USER="$CURRENT_USER" "$NODE_PATH" "$SETUP_SCRIPT" || {
echo ""
echo "β οΈ ComfyUI setup encountered an error"
echo "You can run the setup manually later:"
echo " cd /Applications/FoundryMCPServer.app/Contents/Resources"
echo " node setup-comfyui-headless.js"
echo ""
echo "Installation log: $USER_HOME/foundry-mcp-install.log"
exit 1
}
# Fix permissions on ComfyUI directory so user can run it
echo "Setting ComfyUI permissions for user: $CURRENT_USER"
chown -R "$CURRENT_USER:staff" /Applications/FoundryMCPServer.app/Contents/Resources/ComfyUI/
echo ""
echo "β
ComfyUI installation complete"
exit 0
`;
fs.writeFileSync(path.join(comfyuiScripts, 'postinstall'), comfyuiPostinstall);
fs.chmodSync(path.join(comfyuiScripts, 'postinstall'), 0o755);
// Build ComfyUI component package
execSync(`pkgbuild \\
--root "${COMFYUI_ROOT}" \\
--scripts "${comfyuiScripts}" \\
--identifier com.foundry-mcp.comfyui \\
--version "${VERSION}" \\
--install-location / \\
"${COMFYUI_PKG}"`, {
stdio: 'inherit'
});
console.log('β
ComfyUI component package built');
// ============================================================================
// Create Distribution XML for Component Selection
// ============================================================================
console.log('\nπ Creating distribution XML...');
const distributionXML = `<?xml version="1.0" encoding="utf-8"?>
<installer-gui-script minSpecVersion="2">
<title>Foundry MCP Server</title>
<organization>com.foundry-mcp</organization>
<domains enable_localSystem="true"/>
<options customize="always" require-scripts="true" hostArchitectures="arm64,x86_64"/>
<welcome file="welcome.html"/>
<license file="license.txt"/>
<conclusion file="conclusion.html"/>
<choices-outline>
<line choice="core"/>
<line choice="foundryModule"/>
<line choice="comfyui"/>
</choices-outline>
<choice id="core" visible="true" enabled="false" selected="true" title="MCP Server" description="Core Foundry MCP Server and Claude Desktop integration (Required, ~5MB)">
<pkg-ref id="com.foundry-mcp.core"/>
</choice>
<choice id="foundryModule" visible="true" enabled="true" start_selected="true" title="Foundry MCP Bridge" description="Foundry VTT module for Claude integration (~5MB)">
<pkg-ref id="com.foundry-mcp.foundry-module"/>
</choice>
<choice id="comfyui" visible="true" enabled="true" start_selected="true" title="ComfyUI AI Map Generation" description="AI-powered battlemap generation. Downloads Python, ComfyUI, PyTorch, and AI models (~15GB total, 20-30 min install time)">
<pkg-ref id="com.foundry-mcp.comfyui"/>
</choice>
<pkg-ref id="com.foundry-mcp.core" version="${VERSION}" onConclusion="none">FoundryMCP-Core.pkg</pkg-ref>
<pkg-ref id="com.foundry-mcp.foundry-module" version="${VERSION}" onConclusion="none">FoundryMCP-FoundryModule.pkg</pkg-ref>
<pkg-ref id="com.foundry-mcp.comfyui" version="${VERSION}" onConclusion="none">FoundryMCP-ComfyUI.pkg</pkg-ref>
</installer-gui-script>
`;
const distXMLPath = path.join(BUILD_DIR, 'distribution.xml');
fs.writeFileSync(distXMLPath, distributionXML);
console.log('β
Distribution XML created');
// ============================================================================
// Build Final Product Package
// ============================================================================
console.log('\nπ¨ Building final installer package...');
execSync(`productbuild \\
--distribution "${distXMLPath}" \\
--resources "${BUILD_DIR}" \\
--package-path "${BUILD_DIR}" \\
"${FINAL_PKG}"`, {
stdio: 'inherit'
});
console.log('β
Final package built');
// Clean up
console.log('\nπ§Ή Cleaning up build artifacts...');
[CORE_ROOT, FOUNDRY_ROOT, COMFYUI_ROOT, coreScripts, foundryScripts, comfyuiScripts,
CORE_PKG, FOUNDRY_PKG, COMFYUI_PKG, distXMLPath].forEach(item => {
if (fs.existsSync(item)) {
fs.rmSync(item, { recursive: true, force: true });
}
});
console.log('β
Build artifacts cleaned');
const stats = fs.statSync(FINAL_PKG);
console.log('');
console.log('ββββββββββββββββββββββββββββββββββββββββ');
console.log('β
Mac Installer Build Complete!');
console.log('ββββββββββββββββββββββββββββββββββββββββ');
console.log('');
console.log(`π¦ File: ${FINAL_PKG}`);
console.log(`π Size: ${(stats.size / 1024 / 1024).toFixed(1)} MB`);
console.log('');
console.log('Components:');
console.log(' β
MCP Server (Required)');
console.log(' β
Foundry Module (Optional, Default ON)');
console.log(' β
ComfyUI AI Maps (Optional, Default ON)');
console.log('');