Files
boss/scripts/package-boss-agent-mac-runtime.sh
2026-05-17 02:20:08 +08:00

312 lines
12 KiB
Bash
Executable File

#!/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"