#!/bin/zsh set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" VERSION="${BOSS_AGENT_PACKAGE_VERSION:-$(date +%Y%m%d%H%M%S)}" PACKAGE_NAME="boss-agent-mac-runtime-${VERSION}" DIST_DIR="$ROOT_DIR/dist" STAGE_DIR="$DIST_DIR/$PACKAGE_NAME" RUNTIME_DIR="$STAGE_DIR/runtime" ARCHIVE_PATH="$DIST_DIR/${PACKAGE_NAME}.zip" rm -rf "$STAGE_DIR" "$ARCHIVE_PATH" "$ROOT_DIR/scripts/build-boss-agent-mac-app.sh" >/dev/null mkdir -p "$RUNTIME_DIR/scripts" "$RUNTIME_DIR/local-agent" "$RUNTIME_DIR/deployment/launchd" cp -R "$ROOT_DIR/dist/boss-agent.app" "$STAGE_DIR/boss-agent.app" rsync -a "$ROOT_DIR/local-agent/" "$RUNTIME_DIR/local-agent/" cp "$ROOT_DIR/scripts/start-local-agent.sh" "$RUNTIME_DIR/scripts/start-local-agent.sh" cp "$ROOT_DIR/scripts/install-local-launchagent.sh" "$RUNTIME_DIR/scripts/install-local-launchagent.sh" cp "$ROOT_DIR/scripts/browser-control-smoke.mjs" "$RUNTIME_DIR/scripts/browser-control-smoke.mjs" cp "$ROOT_DIR/scripts/codex-computer-use-runtime.mjs" "$RUNTIME_DIR/scripts/codex-computer-use-runtime.mjs" cp "$ROOT_DIR/scripts/computer-use-smoke.mjs" "$RUNTIME_DIR/scripts/computer-use-smoke.mjs" cp "$ROOT_DIR/scripts/cua-driver-computer-use-runtime.mjs" "$RUNTIME_DIR/scripts/cua-driver-computer-use-runtime.mjs" cp "$ROOT_DIR/scripts/codex-desktop-refresh-hint.mjs" "$RUNTIME_DIR/scripts/codex-desktop-refresh-hint.mjs" cp "$ROOT_DIR/scripts/codex-desktop-refresh-bridge-daemon.mjs" "$RUNTIME_DIR/scripts/codex-desktop-refresh-bridge-daemon.mjs" cp "$ROOT_DIR/deployment/launchd/com.hyzq.boss.local-agent.plist" "$RUNTIME_DIR/deployment/launchd/com.hyzq.boss.local-agent.plist" cp "$ROOT_DIR/deployment/launchd/com.hyzq.boss.codex-desktop-bridge.plist" "$RUNTIME_DIR/deployment/launchd/com.hyzq.boss.codex-desktop-bridge.plist" node - <<'NODE' "$RUNTIME_DIR" "$VERSION" const fs = require("fs"); const path = require("path"); const runtimeDir = process.argv[2]; const version = process.argv[3]; for (const name of ["config.cloud.json", "config.example.json"]) { const configPath = path.join(runtimeDir, "local-agent", name); if (!fs.existsSync(configPath)) continue; const config = JSON.parse(fs.readFileSync(configPath, "utf8")); config.bossAgentOtaEnabled = true; config.bossAgentVersion = version; config.bossAgentOtaAutoInstall = false; config.bossAgentOtaCheckIntervalMs = config.bossAgentOtaCheckIntervalMs || 300000; fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`); } NODE cat > "$RUNTIME_DIR/package.json" <<'JSON' { "name": "boss-agent-runtime", "version": "0.1.0", "private": true, "dependencies": { "qrcode": "^1.5.4" } } JSON node - <<'NODE' "$ROOT_DIR" "$RUNTIME_DIR" const fs = require("fs"); const path = require("path"); const rootDir = process.argv[2]; const runtimeDir = process.argv[3]; const sourceModules = path.join(rootDir, "node_modules"); const targetModules = path.join(runtimeDir, "node_modules"); const seen = new Set(); function readPackage(name) { return JSON.parse(fs.readFileSync(path.join(sourceModules, name, "package.json"), "utf8")); } function walk(name) { if (seen.has(name)) return; seen.add(name); const pkg = readPackage(name); for (const dep of Object.keys(pkg.dependencies || {})) { walk(dep); } } walk("qrcode"); fs.mkdirSync(targetModules, { recursive: true }); for (const name of seen) { fs.cpSync(path.join(sourceModules, name), path.join(targetModules, name), { recursive: true, dereference: true, filter: (source) => !/\/(test|tests|example|examples|\.github)\b/.test(source), }); } NODE cat > "$STAGE_DIR/install.command" <<'SH' #!/bin/zsh set -euo pipefail SOURCE_DIR="$(cd "$(dirname "$0")" && pwd)" INSTALL_ROOT="${BOSS_AGENT_INSTALL_ROOT:-$HOME/boss-agent/current}" APP_TARGET_DIR="${BOSS_AGENT_APP_TARGET_DIR:-$HOME/Applications}" APP_TARGET="$APP_TARGET_DIR/boss-agent.app" CONFIG_BACKUP_DIR="" ACTIVE_CONFIG_BASENAME="config.cloud.json" ACTIVE_PLIST="$HOME/Library/LaunchAgents/com.hyzq.boss.local-agent.plist" NODE_BIN="${BOSS_NODE_BIN:-}" if [[ -f "$ACTIVE_PLIST" ]]; then ACTIVE_CONFIG_PATH="$(/usr/libexec/PlistBuddy -c 'Print :ProgramArguments:2' "$ACTIVE_PLIST" 2>/dev/null || true)" if [[ -n "$ACTIVE_CONFIG_PATH" && "$ACTIVE_CONFIG_PATH" == "$INSTALL_ROOT/local-agent/"*.json ]]; then ACTIVE_CONFIG_BASENAME="$(basename "$ACTIVE_CONFIG_PATH")" fi fi if [[ -z "$NODE_BIN" ]]; then NODE_BIN="$(command -v node 2>/dev/null || true)" fi if [[ -z "$NODE_BIN" ]]; then NODE_CANDIDATES=("$HOME"/.boss-runtime/node-*/bin/node(N) /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node) for candidate in "${NODE_CANDIDATES[@]}"; do if [[ -x "$candidate" ]]; then NODE_BIN="$candidate" break fi done fi if [[ -z "$NODE_BIN" || ! -x "$NODE_BIN" ]]; then echo "Node.js is required. Install Node.js 22 or newer first." >&2 exit 1 fi NODE_MAJOR="$("$NODE_BIN" -p 'Number(process.versions.node.split(".")[0])')" if [[ "$NODE_MAJOR" -lt 22 ]]; then echo "Node.js 22 or newer is required. Current: $("$NODE_BIN" -v)" >&2 exit 1 fi EXISTING_CONFIGS=("$INSTALL_ROOT"/local-agent/config*.json(N)) if [[ "${#EXISTING_CONFIGS[@]}" -gt 0 ]]; then CONFIG_BACKUP_DIR="$(mktemp -d)" cp "${EXISTING_CONFIGS[@]}" "$CONFIG_BACKUP_DIR"/ fi mkdir -p "$(dirname "$INSTALL_ROOT")" "$APP_TARGET_DIR" rsync -a --delete "$SOURCE_DIR/runtime/" "$INSTALL_ROOT/" if [[ -n "$CONFIG_BACKUP_DIR" && -d "$CONFIG_BACKUP_DIR" ]]; then cp "$CONFIG_BACKUP_DIR"/config*.json "$INSTALL_ROOT/local-agent/" rm -rf "$CONFIG_BACKUP_DIR" fi PACKAGE_VERSION="__BOSS_AGENT_PACKAGE_VERSION__" "$NODE_BIN" - <<'NODE' "$INSTALL_ROOT/local-agent" "$INSTALL_ROOT" "$PACKAGE_VERSION" const fs = require("fs"); const os = require("os"); const path = require("path"); const configDir = process.argv[2]; const installRoot = process.argv[3]; const version = process.argv[4]; const home = os.homedir(); const configPaths = fs.readdirSync(configDir) .filter((name) => /^config.*\.json$/.test(name)) .map((name) => path.join(configDir, name)); for (const configPath of configPaths) { const config = JSON.parse(fs.readFileSync(configPath, "utf8")); const nodeCommand = config.computerUseCommand && path.isAbsolute(config.computerUseCommand) ? config.computerUseCommand : "node"; config.bossAgentOtaEnabled = true; config.bossAgentVersion = version; config.bossAgentInstallRoot = installRoot; config.bossAgentOtaDownloadDir = path.join(home, "boss-agent", "updates"); config.bossAgentOtaCheckIntervalMs = config.bossAgentOtaCheckIntervalMs || 300000; config.bossAgentOtaAutoInstall = false; config.skillsDir = config.skillsDir || path.join(home, ".codex", "skills"); config.codexAppServerEnabled = config.codexAppServerEnabled !== false; config.codexAppServerCommand = config.codexAppServerCommand || "codex"; config.codexAppServerArgs = Array.isArray(config.codexAppServerArgs) ? config.codexAppServerArgs : ["app-server"]; config.codexAppServerTimeoutMs = config.codexAppServerTimeoutMs || 120000; config.codexAppServerFallbackToCli = config.codexAppServerFallbackToCli !== false; config.codexComputerUseEnabled = config.codexComputerUseEnabled !== false; config.codexComputerUseCommand = config.codexComputerUseCommand || nodeCommand; config.codexComputerUseArgs = Array.isArray(config.codexComputerUseArgs) ? config.codexComputerUseArgs : ["scripts/codex-computer-use-runtime.mjs"]; config.codexComputerUseTimeoutMs = config.codexComputerUseTimeoutMs || 120000; config.codexComputerUseFallbackToCua = config.codexComputerUseFallbackToCua !== false; if (!Array.isArray(config.computerUseArgs) || config.computerUseArgs.length === 0 || config.computerUseArgs.includes("scripts/computer-use-smoke.mjs")) { config.computerUseArgs = ["scripts/cua-driver-computer-use-runtime.mjs"]; } config.cuaDriverCommand = config.cuaDriverCommand || "cua-driver"; config.cuaDriverArgs = Array.isArray(config.cuaDriverArgs) ? config.cuaDriverArgs : []; config.cuaDriverTimeoutMs = config.cuaDriverTimeoutMs || 45000; for (const key of [ "masterAgentWorkdir", "codexAppServerWorkdir", "codexComputerUseWorkdir", "browserControlWorkdir", "computerUseWorkdir", "codexDesktopRefreshWorkdir", "omxWorkdir" ]) { config[key] = installRoot; } fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`); } NODE if [[ "$ACTIVE_CONFIG_BASENAME" == "config.installed.json" || "$ACTIVE_CONFIG_BASENAME" == "config.cloud.json" || "$ACTIVE_CONFIG_BASENAME" == "config.example.json" ]]; then CUSTOM_CONFIGS=("$INSTALL_ROOT"/local-agent/config*.json(N)) for custom_config in "${CUSTOM_CONFIGS[@]}"; do custom_name="$(basename "$custom_config")" case "$custom_name" in config.installed.json|config.cloud.json|config.example.json) continue ;; esac if "$NODE_BIN" - "$custom_config" <<'NODE'; then const fs = require("fs"); const configPath = process.argv[2]; try { const config = JSON.parse(fs.readFileSync(configPath, "utf8")); process.exit(config.deviceId && config.token ? 0 : 1); } catch { process.exit(1); } NODE ACTIVE_CONFIG_BASENAME="$custom_name" break fi done fi if [[ ! -f "$INSTALL_ROOT/local-agent/$ACTIVE_CONFIG_BASENAME" ]]; then ACTIVE_CONFIG_BASENAME="config.cloud.json" fi chmod +x "$INSTALL_ROOT/scripts/start-local-agent.sh" chmod +x "$INSTALL_ROOT/scripts/install-local-launchagent.sh" chmod +x "$INSTALL_ROOT/scripts/"*.mjs rm -rf "$APP_TARGET" cp -R "$SOURCE_DIR/boss-agent.app" "$APP_TARGET" "$INSTALL_ROOT/scripts/install-local-launchagent.sh" "$INSTALL_ROOT/local-agent/$ACTIVE_CONFIG_BASENAME" echo "boss-agent installed:" echo " runtime: $INSTALL_ROOT" echo " app: $APP_TARGET" echo "Open $APP_TARGET to view status." SH perl -0pi -e "s/__BOSS_AGENT_PACKAGE_VERSION__/$VERSION/g" "$STAGE_DIR/install.command" chmod +x "$STAGE_DIR/install.command" cat > "$STAGE_DIR/README_INSTALL.txt" <<'TXT' Boss Agent macOS runtime package Install: 1. Unzip this package on the controlled Mac. 2. Double click install.command. 3. Open ~/Applications/boss-agent.app. Notes: - Existing ~/boss-agent/current/local-agent/config.cloud.json is preserved. - The installer updates bossAgentVersion and local runtime paths after preserving binding credentials. - boss-agent OTA downloads future macOS runtime packages from the Boss server, verifies sha256, then launches this installer flow again. - Node.js 22 or newer is required because the local agent uses node:sqlite. - This build includes the Cua Driver desktop control runtime. Install and authorize `cua-driver` on the controlled Mac before enabling full desktop GUI control. TXT ( cd "$DIST_DIR" ditto -c -k --sequesterRsrc --keepParent "$PACKAGE_NAME" "$ARCHIVE_PATH" ) node - <<'NODE' "$ARCHIVE_PATH" "$ROOT_DIR" "$VERSION" const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); const archivePath = process.argv[2]; const rootDir = process.argv[3]; const version = process.argv[4]; const downloadsDir = path.join(rootDir, "public", "downloads"); const latestPath = path.join(downloadsDir, "boss-agent-mac-latest.zip"); const latestMetaPath = path.join(downloadsDir, "boss-agent-mac-latest.json"); fs.mkdirSync(downloadsDir, { recursive: true }); fs.copyFileSync(archivePath, latestPath); const content = fs.readFileSync(latestPath); const stat = fs.statSync(latestPath); const meta = { packageType: "boss_agent_macos", version, fileName: "boss-agent-mac-latest.zip", archiveFileName: path.basename(archivePath), sizeBytes: stat.size, sha256: crypto.createHash("sha256").update(content).digest("hex"), updatedAt: stat.mtime.toISOString(), downloadUrl: "/api/v1/boss-agent/ota/package" }; fs.writeFileSync(latestMetaPath, `${JSON.stringify(meta, null, 2)}\n`); NODE echo "$ARCHIVE_PATH"