agent-desktop/npm/scripts/postinstall.js
Lahfir 1291a9cdbf
refactor!: unify command execution contracts
Unify CLI and batch dispatch around the typed command path, centralize command policy and ref resolution, harden macOS action verification, split command tests from implementation, and add package/release guardrails.

BREAKING CHANGE: CLI and batch execution now share the typed command path and current command argument contracts.

BREAKING CHANGE: Ref-consuming commands use snapshot-scoped refs; deterministic consumers should pass snapshot_id and handle SNAPSHOT_NOT_FOUND.

BREAKING CHANGE: permissions and status now return PermissionReport fields for accessibility, screen_recording, and automation instead of a single boolean status.

BREAKING CHANGE: PermissionState gains NotRequired; macOS automation now reports not_required instead of unknown.

BREAKING CHANGE: right-click now separates action success from menu verification; consumers should inspect menu or menu_probe instead of assuming every right-click returns an inline menu.

BREAKING CHANGE: focus-window now confirms OS focus and returns ACTION_FAILED when focus does not settle; data.focused.is_focused is true on success.

BREAKING CHANGE: PlatformAdapter::execute_action now takes ActionRequest, and permission probing uses permission_report/request_permissions.

BREAKING CHANGE: FFI ad_execute_action now defaults to headless policy. Consumers that need focus fallback or cursor-moving behavior must call ad_execute_action_with_policy with AD_POLICY_KIND_FOCUS_FALLBACK or AD_POLICY_KIND_PHYSICAL.

BREAKING CHANGE: FFI ad_check_permissions no longer treats unknown accessibility permission as success; stub-style unknown probes return ERR_PLATFORM_NOT_SUPPORTED and macOS ambiguous unknown returns ERR_INTERNAL with last-error detail.

BREAKING CHANGE: JSON response envelopes now report version 2.0; parsers pinned to 1.0 must branch or update.

BREAKING CHANGE: focus now uses accessibility focus without cursor movement; callers that need physical focus must use explicit mouse or physical-policy paths.

BREAKING CHANGE: chain execution deadlines now return TIMEOUT instead of ACTION_FAILED when the target app does not respond before the chain deadline.
2026-05-19 18:27:08 -07:00

213 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
const { existsSync, mkdirSync, chmodSync, unlinkSync, renameSync, writeFileSync, symlinkSync, lstatSync } = require('fs');
const { readFileSync } = require('fs');
const { join } = require('path');
const { platform, arch } = require('os');
const { execFileSync } = require('child_process');
const { createHash } = require('crypto');
const projectRoot = join(__dirname, '..');
const binDir = join(projectRoot, 'bin');
const packageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
const version = packageJson.version;
const GITHUB_REPO = 'lahfir/agent-desktop';
const TARGET_MAP = {
'darwin-arm64': 'aarch64-apple-darwin',
'darwin-x64': 'x86_64-apple-darwin',
'linux-x64': 'x86_64-unknown-linux-gnu',
'linux-arm64': 'aarch64-unknown-linux-gnu',
'win32-x64': 'x86_64-pc-windows-msvc',
};
const BINARY_NAME_MAP = {
'darwin-arm64': 'agent-desktop-darwin-arm64',
'darwin-x64': 'agent-desktop-darwin-x64',
'linux-x64': 'agent-desktop-linux-x64',
'linux-arm64': 'agent-desktop-linux-arm64',
'win32-x64': 'agent-desktop-win32-x64.exe',
};
const SUPPORTED_PLATFORMS = ['darwin'];
function log(msg) {
process.stderr.write(`agent-desktop: ${msg}\n`);
}
function getPlatformKey() {
return `${platform()}-${arch()}`;
}
function download(url, dest) {
const tmpDest = dest + '.tmp';
try {
execFileSync('curl', ['-fsSL', '--retry', '3', '--retry-delay', '2', '-o', tmpDest, url], {
stdio: 'pipe',
timeout: 60000,
});
renameSync(tmpDest, dest);
} catch (err) {
try { unlinkSync(tmpDest); } catch {}
throw new Error(`Failed to download ${url}: ${err.message}`);
}
}
function verifyChecksum(filePath, expectedHash) {
const fileBuffer = readFileSync(filePath);
const hash = createHash('sha256').update(fileBuffer).digest('hex');
return hash === expectedHash;
}
function fixGlobalInstallBin() {
if (platform() === 'win32') return;
let npmBinDir;
try {
const prefix = execFileSync('npm', ['prefix', '-g'], { encoding: 'utf8', timeout: 5000 }).trim();
npmBinDir = join(prefix, 'bin');
} catch {
return;
}
const symlinkPath = join(npmBinDir, 'agent-desktop');
const platformKey = getPlatformKey();
const binaryName = BINARY_NAME_MAP[platformKey];
if (!binaryName) return;
const binaryPath = join(binDir, binaryName);
try {
const stat = lstatSync(symlinkPath);
if (!stat.isSymbolicLink()) return;
} catch {
return;
}
try {
unlinkSync(symlinkPath);
symlinkSync(binaryPath, symlinkPath);
log('Optimized: symlink points to native binary (zero overhead)');
} catch (err) {
log(`Could not optimize symlink: ${err.message}`);
}
}
function promptSkillInstall() {
const platformSkill = {
darwin: 'agent-desktop-macos',
win32: 'agent-desktop-windows',
linux: 'agent-desktop-linux',
}[platform()];
log('');
log('Claude Code skills available for agent-desktop!');
log('Install with:');
log(' claude mcp add-skill lahfir/agent-desktop');
if (platformSkill) {
log(` claude mcp add-skill lahfir/${platformSkill}`);
}
log('');
}
function main() {
if (process.env.AGENT_DESKTOP_SKIP_DOWNLOAD === '1') {
log('Skipping binary download (AGENT_DESKTOP_SKIP_DOWNLOAD=1)');
return;
}
const platformKey = getPlatformKey();
const target = TARGET_MAP[platformKey];
const binaryName = BINARY_NAME_MAP[platformKey];
if (!SUPPORTED_PLATFORMS.includes(platform()) || !target || !binaryName) {
log('agent-desktop currently supports macOS only.');
log('Windows and Linux support is coming in Phase 2.');
log(`See: https://github.com/${GITHUB_REPO}`);
return;
}
const binaryPath = join(binDir, binaryName);
if (process.env.AGENT_DESKTOP_BINARY_PATH) {
const customPath = process.env.AGENT_DESKTOP_BINARY_PATH;
if (existsSync(customPath)) {
try {
writeFileSync(binaryPath, readFileSync(customPath));
chmodSync(binaryPath, 0o755);
log(`Using binary from AGENT_DESKTOP_BINARY_PATH: ${customPath}`);
fixGlobalInstallBin();
promptSkillInstall();
return;
} catch (err) {
log(`Failed to copy from AGENT_DESKTOP_BINARY_PATH: ${err.message}`);
}
}
}
if (existsSync(binaryPath)) {
chmodSync(binaryPath, 0o755);
log(`Native binary ready: ${binaryName}`);
fixGlobalInstallBin();
promptSkillInstall();
return;
}
if (!existsSync(binDir)) {
mkdirSync(binDir, { recursive: true });
}
const tarball = `agent-desktop-v${version}-${target}.tar.gz`;
const baseUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${version}`;
const tarballUrl = `${baseUrl}/${tarball}`;
const checksumsUrl = `${baseUrl}/checksums.txt`;
const tarballPath = join(binDir, tarball);
const checksumsPath = join(binDir, 'checksums.txt');
log(`Downloading native binary for ${platformKey}...`);
try {
download(tarballUrl, tarballPath);
download(checksumsUrl, checksumsPath);
const checksums = readFileSync(checksumsPath, 'utf8');
const expectedLine = checksums.split('\n').find((line) => line.includes(tarball));
if (!expectedLine) {
throw new Error(`Checksum entry missing for ${tarball}`);
}
const expectedHash = expectedLine.split(/\s+/)[0];
if (!verifyChecksum(tarballPath, expectedHash)) {
throw new Error('Checksum verification failed');
}
unlinkSync(checksumsPath);
log('Checksum verified');
execFileSync('tar', ['-xzf', tarballPath, '-C', binDir], { stdio: 'pipe' });
const extractedBinary = join(binDir, 'agent-desktop');
if (existsSync(extractedBinary) && extractedBinary !== binaryPath) {
renameSync(extractedBinary, binaryPath);
}
chmodSync(binaryPath, 0o755);
unlinkSync(tarballPath);
log(`Installed native binary: ${binaryName}`);
} catch (err) {
log(`Could not download native binary: ${err.message}`);
log('');
log('Download manually from:');
log(` ${tarballUrl}`);
log(`Then place at: ${binaryPath}`);
try { if (existsSync(tarballPath)) unlinkSync(tarballPath); } catch {}
try { if (existsSync(checksumsPath)) unlinkSync(checksumsPath); } catch {}
process.exitCode = 1;
return;
}
fixGlobalInstallBin();
promptSkillInstall();
}
main();