mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-07-29 18:29:20 +00:00
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.
213 lines
6.2 KiB
JavaScript
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();
|