312 lines
12 KiB
Bash
Executable File
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"
|