mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-08-16 12:03:46 +00:00
feat: add release automation with GitHub Releases and npm distribution
Automated release pipeline using release-please for gated Release PRs, conventional commits for SemVer version bumps, and npm distribution with postinstall binary download from GitHub Releases.
This commit is contained in:
parent
aa3b210708
commit
1d1a7fee3b
8 changed files with 654 additions and 11 deletions
160
.github/workflows/release.yml
vendored
Normal file
160
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
release-please:
|
||||
name: Release Please
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
outputs:
|
||||
release_created: ${{ steps.release.outputs.release_created }}
|
||||
tag_name: ${{ steps.release.outputs.tag_name }}
|
||||
version: ${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}
|
||||
steps:
|
||||
- uses: googleapis/release-please-action@v4
|
||||
id: release
|
||||
with:
|
||||
config-file: release-please-config.json
|
||||
manifest-file: .release-please-manifest.json
|
||||
|
||||
build:
|
||||
name: Build (${{ matrix.target }})
|
||||
needs: release-please
|
||||
if: needs.release-please.outputs.release_created == 'true'
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-apple-darwin
|
||||
runner: macos-latest
|
||||
- target: x86_64-apple-darwin
|
||||
runner: macos-13
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release-please.outputs.tag_name }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
run: |
|
||||
rustup show
|
||||
rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
||||
restore-keys: ${{ runner.os }}-${{ matrix.target }}-cargo-
|
||||
|
||||
- name: Build release binary
|
||||
run: cargo build --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Check binary size
|
||||
run: |
|
||||
SIZE=$(stat -f%z target/${{ matrix.target }}/release/agent-desktop)
|
||||
LIMIT=$((15 * 1024 * 1024))
|
||||
echo "Binary size: $(du -sh target/${{ matrix.target }}/release/agent-desktop | cut -f1)"
|
||||
if [ "$SIZE" -gt "$LIMIT" ]; then
|
||||
echo "FAIL: binary exceeds 15MB limit (${SIZE} bytes)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create tarball
|
||||
run: |
|
||||
VERSION=${{ needs.release-please.outputs.version }}
|
||||
TARBALL="agent-desktop-v${VERSION}-${{ matrix.target }}.tar.gz"
|
||||
tar -czf "${TARBALL}" -C target/${{ matrix.target }}/release agent-desktop
|
||||
shasum -a 256 "${TARBALL}" > "${TARBALL}.sha256"
|
||||
echo "TARBALL=${TARBALL}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binary-${{ matrix.target }}
|
||||
path: |
|
||||
agent-desktop-v*.tar.gz
|
||||
agent-desktop-v*.tar.gz.sha256
|
||||
|
||||
publish-github:
|
||||
name: Publish to GitHub Release
|
||||
needs: [release-please, build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: binary-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Create checksums file
|
||||
run: |
|
||||
cat *.sha256 > checksums.txt
|
||||
echo "Checksums:"
|
||||
cat checksums.txt
|
||||
|
||||
- name: Upload assets to GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
gh release upload ${{ needs.release-please.outputs.tag_name }} \
|
||||
*.tar.gz checksums.txt \
|
||||
--clobber
|
||||
|
||||
publish-npm:
|
||||
name: Publish to npm
|
||||
needs: [release-please, publish-github]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release-please.outputs.tag_name }}
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Verify GitHub Release assets
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
ASSETS=$(gh release view ${{ needs.release-please.outputs.tag_name }} --json assets --jq '.assets | length')
|
||||
if [ "$ASSETS" -lt 3 ]; then
|
||||
echo "FAIL: expected at least 3 assets (2 tarballs + checksums), found ${ASSETS}"
|
||||
exit 1
|
||||
fi
|
||||
echo "OK: ${ASSETS} assets found on release"
|
||||
|
||||
- name: Sync version to package.json
|
||||
run: |
|
||||
VERSION=${{ needs.release-please.outputs.version }}
|
||||
cd npm
|
||||
node -e "
|
||||
const pkg = require('./package.json');
|
||||
pkg.version = '${VERSION}';
|
||||
require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
echo "npm package version set to ${VERSION}"
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish --provenance --access public
|
||||
working-directory: npm
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -62,6 +62,11 @@ dist/
|
|||
*.rpm
|
||||
*.snap
|
||||
|
||||
# npm (platform binaries downloaded by postinstall)
|
||||
npm/bin/agent-desktop-*
|
||||
npm/node_modules/
|
||||
node_modules/
|
||||
|
||||
# Compound Engineering
|
||||
docs/
|
||||
todos/
|
||||
|
|
|
|||
3
.release-please-manifest.json
Normal file
3
.release-please-manifest.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
".": "0.1.0"
|
||||
}
|
||||
61
CLAUDE.md
61
CLAUDE.md
|
|
@ -1,4 +1,25 @@
|
|||
# agent-desktop
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
cargo build # Debug build
|
||||
cargo build --release # Release build (<15MB target)
|
||||
cargo test --lib --workspace # Run all unit tests
|
||||
cargo test --lib -p agent-desktop-core # Test core crate only
|
||||
cargo test --lib -p agent-desktop-macos # Test macOS crate only
|
||||
cargo test test_name # Run a single test by name
|
||||
cargo clippy --all-targets -- -D warnings # Lint (must pass, zero warnings)
|
||||
cargo fmt --all -- --check # Format check
|
||||
cargo fmt --all # Auto-format
|
||||
cargo tree -p agent-desktop-core # Verify no platform crate leaks (CI enforces)
|
||||
```
|
||||
|
||||
Run the binary: `./target/release/agent-desktop snapshot --app Finder -i`
|
||||
|
||||
## Project Overview
|
||||
|
||||
Cross-platform Rust CLI + MCP server enabling AI agents to observe and control desktop applications via native OS accessibility trees.
|
||||
|
||||
|
|
@ -7,7 +28,19 @@ Cross-platform Rust CLI + MCP server enabling AI agents to observe and control d
|
|||
- All commits are authored by **Lahfir**
|
||||
- NEVER add `Co-Authored-By` lines, AI attribution badges, or "Generated with" footers
|
||||
- NEVER include co-committers of any kind
|
||||
- Commit messages: concise, imperative mood, focus on "why" not "what"
|
||||
- **Conventional Commits required.** Every commit message must use a type prefix:
|
||||
- `feat:` — new feature (triggers minor version bump)
|
||||
- `fix:` — bug fix (triggers patch version bump)
|
||||
- `feat!:` or `BREAKING CHANGE:` footer — breaking change (triggers major version bump)
|
||||
- `docs:` — documentation only
|
||||
- `style:` — formatting, no code change
|
||||
- `refactor:` — code change that neither fixes a bug nor adds a feature
|
||||
- `chore:` — maintenance tasks, dependencies
|
||||
- `ci:` — CI/CD changes
|
||||
- `test:` — adding or fixing tests
|
||||
- Format: `type: concise imperative description` (lowercase type, no capital after colon)
|
||||
- Focus on "why" not "what"
|
||||
- Examples: `feat: add scroll-to command`, `fix: prevent stale ref on window resize`, `ci: add binary size check`
|
||||
|
||||
## Core Principle
|
||||
|
||||
|
|
@ -28,8 +61,11 @@ agent-desktop/
|
|||
│ ├── windows/ # agent-desktop-windows (stub → Phase 2)
|
||||
│ └── linux/ # agent-desktop-linux (stub → Phase 2)
|
||||
├── src/ # agent-desktop binary (entry point)
|
||||
│ ├── main.rs # mode detection, dispatch
|
||||
│ └── cli.rs # clap derive structs
|
||||
│ ├── main.rs # entry point, permission check, JSON envelope
|
||||
│ ├── cli.rs # clap derive enum (Commands)
|
||||
│ ├── cli_args.rs # all command argument structs
|
||||
│ ├── dispatch.rs # command dispatcher + parse helpers
|
||||
│ └── batch_dispatch.rs # batch command execution
|
||||
└── tests/
|
||||
├── fixtures/ # golden JSON snapshots
|
||||
└── integration/ # macOS CI integration tests
|
||||
|
|
@ -370,17 +406,20 @@ Target binary size: <15MB per platform.
|
|||
- `cargo test --workspace`
|
||||
- Binary size check: fail if release binary exceeds 15MB
|
||||
|
||||
## Phase 1 Command Scope (30 commands)
|
||||
## Implemented Commands (50)
|
||||
|
||||
| Category | Commands |
|
||||
|----------|----------|
|
||||
| App/Window (5) | `launch`, `close-app`, `list-windows`, `list-apps`, `focus-window` |
|
||||
| Observation (15) | `snapshot`, `screenshot`, `find`, `get` (text, value, title, bounds, role, states), `is` (visible, enabled, checked, focused, expanded) |
|
||||
| Interaction (11) | `click`, `double-click`, `right-click`, `type`, `set-value`, `focus`, `select`, `toggle`, `expand`, `collapse`, `scroll` |
|
||||
| Keyboard (1) | `press` |
|
||||
| Clipboard (2) | `clipboard get`, `clipboard set` |
|
||||
| Wait (3) | `wait` (ms), `wait --element`, `wait --window` |
|
||||
| App/Window (10) | `launch`, `close-app`, `list-windows`, `list-apps`, `focus-window`, `resize-window`, `move-window`, `minimize`, `maximize`, `restore` |
|
||||
| Observation (6) | `snapshot`, `screenshot`, `find`, `get`, `is`, `list-surfaces` |
|
||||
| Interaction (14) | `click`, `double-click`, `triple-click`, `right-click`, `type`, `set-value`, `clear`, `focus`, `select`, `toggle`, `check`, `uncheck`, `expand`, `collapse` |
|
||||
| Scroll (2) | `scroll`, `scroll-to` |
|
||||
| Keyboard (3) | `press`, `key-down`, `key-up` |
|
||||
| Mouse (5) | `hover`, `drag`, `mouse-move`, `mouse-click`, `mouse-down`, `mouse-up` |
|
||||
| Clipboard (3) | `clipboard-get`, `clipboard-set`, `clipboard-clear` |
|
||||
| Wait (1) | `wait` (with `--element`, `--window`, `--text`, `--menu` flags) |
|
||||
| System (3) | `status`, `permissions`, `version` |
|
||||
| Batch (1) | `batch` |
|
||||
|
||||
## Non-Goals
|
||||
|
||||
|
|
|
|||
86
npm/bin/agent-desktop.js
Executable file
86
npm/bin/agent-desktop.js
Executable file
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const { existsSync, accessSync, chmodSync, constants } = require('fs');
|
||||
const { dirname, join } = require('path');
|
||||
const { platform, arch } = require('os');
|
||||
|
||||
const binDir = __dirname;
|
||||
|
||||
function getBinaryName() {
|
||||
const os = platform();
|
||||
const cpuArch = arch();
|
||||
|
||||
const platformMap = {
|
||||
'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',
|
||||
};
|
||||
|
||||
return platformMap[`${os}-${cpuArch}`] || null;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const binaryName = getBinaryName();
|
||||
|
||||
if (!binaryName) {
|
||||
console.error(`Error: Unsupported platform: ${platform()}-${arch()}`);
|
||||
console.error('agent-desktop currently supports: macOS (ARM64, x64)');
|
||||
console.error('Windows and Linux support is coming in Phase 2.');
|
||||
console.error('See: https://github.com/lahfir/agent-desktop');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const binaryPath = join(binDir, binaryName);
|
||||
|
||||
if (!binaryPath || !existsSync(binaryPath)) {
|
||||
console.error(`Error: Native binary not found for ${platform()}-${arch()}`);
|
||||
console.error(`Expected: ${binaryPath}`);
|
||||
console.error('');
|
||||
console.error('Try reinstalling:');
|
||||
console.error(' npm install -g agent-desktop');
|
||||
console.error('');
|
||||
console.error('Or download directly from:');
|
||||
console.error(' https://github.com/lahfir/agent-desktop/releases');
|
||||
|
||||
if (typeof process.versions.bun !== 'undefined') {
|
||||
console.error('');
|
||||
console.error('Bun detected — postinstall scripts require --trust:');
|
||||
console.error(' bun install -g --trust agent-desktop');
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (platform() !== 'win32') {
|
||||
try {
|
||||
accessSync(binaryPath, constants.X_OK);
|
||||
} catch {
|
||||
try {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
} catch (err) {
|
||||
console.error(`Error: Cannot make binary executable: ${err.message}`);
|
||||
console.error('Try running: chmod +x ' + binaryPath);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: 'inherit',
|
||||
windowsHide: false,
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error(`Error executing binary: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
process.exit(code ?? 0);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
33
npm/package.json
Normal file
33
npm/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "agent-desktop",
|
||||
"version": "0.0.0-development",
|
||||
"description": "AI agent tool for observing and controlling desktop applications via native OS accessibility trees",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/lahfir/agent-desktop.git"
|
||||
},
|
||||
"homepage": "https://github.com/lahfir/agent-desktop",
|
||||
"bin": {
|
||||
"agent-desktop": "./bin/agent-desktop.js"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "node scripts/postinstall.js"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"scripts"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"keywords": [
|
||||
"desktop",
|
||||
"automation",
|
||||
"accessibility",
|
||||
"cli",
|
||||
"agent",
|
||||
"macos",
|
||||
"a11y"
|
||||
]
|
||||
}
|
||||
288
npm/scripts/postinstall.js
Normal file
288
npm/scripts/postinstall.js
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { existsSync, mkdirSync, chmodSync, createWriteStream, unlinkSync, renameSync, writeFileSync, symlinkSync, lstatSync } = require('fs');
|
||||
const { readFileSync } = require('fs');
|
||||
const { dirname, join } = require('path');
|
||||
const { platform, arch } = require('os');
|
||||
const { get } = require('https');
|
||||
const { execSync } = 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 MAX_RETRIES = 3;
|
||||
const TIMEOUT_MS = 60000;
|
||||
|
||||
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 downloadWithRedirects(url, dest, timeout) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`Download timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
|
||||
const doRequest = (requestUrl) => {
|
||||
const parsedUrl = new URL(requestUrl);
|
||||
const options = {
|
||||
hostname: parsedUrl.hostname,
|
||||
path: parsedUrl.pathname + parsedUrl.search,
|
||||
headers: { 'User-Agent': `agent-desktop/${version}` },
|
||||
};
|
||||
|
||||
const proxy = process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy;
|
||||
if (proxy) {
|
||||
log(`Using proxy: ${proxy}`);
|
||||
}
|
||||
|
||||
get(requestUrl, options, (response) => {
|
||||
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||
doRequest(response.headers.location);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`HTTP ${response.statusCode} downloading ${requestUrl}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const tmpDest = dest + '.tmp';
|
||||
const file = createWriteStream(tmpDest);
|
||||
response.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
clearTimeout(timer);
|
||||
try {
|
||||
renameSync(tmpDest, dest);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
doRequest(url);
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadWithRetry(url, dest) {
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
await downloadWithRedirects(url, dest, TIMEOUT_MS);
|
||||
return;
|
||||
} catch (err) {
|
||||
if (attempt === MAX_RETRIES) throw err;
|
||||
const delay = Math.pow(2, attempt) * 1000;
|
||||
log(`Download failed (attempt ${attempt}/${MAX_RETRIES}): ${err.message}`);
|
||||
log(`Retrying in ${delay / 1000}s...`);
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readFileContent(path) {
|
||||
return readFileSync(path, 'utf8');
|
||||
}
|
||||
|
||||
function verifyChecksum(filePath, expectedHash) {
|
||||
const fileBuffer = readFileSync(filePath);
|
||||
const hash = createHash('sha256').update(fileBuffer).digest('hex');
|
||||
return hash === expectedHash;
|
||||
}
|
||||
|
||||
async function fixGlobalInstallBin() {
|
||||
if (platform() === 'win32') return;
|
||||
|
||||
let npmBinDir;
|
||||
try {
|
||||
const prefix = execSync('npm prefix -g', { encoding: 'utf8' }).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}`);
|
||||
log('CLI will work via Node.js wrapper (slightly slower startup)');
|
||||
}
|
||||
}
|
||||
|
||||
async 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 (!target || !binaryName) {
|
||||
if (!SUPPORTED_PLATFORMS.includes(platform())) {
|
||||
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;
|
||||
}
|
||||
log(`Unsupported architecture: ${platformKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SUPPORTED_PLATFORMS.includes(platform())) {
|
||||
log(`agent-desktop currently supports macOS only.`);
|
||||
log(`${platform()} 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 {
|
||||
const content = readFileSync(customPath);
|
||||
writeFileSync(binaryPath, content);
|
||||
chmodSync(binaryPath, 0o755);
|
||||
log(`Using binary from AGENT_DESKTOP_BINARY_PATH: ${customPath}`);
|
||||
await fixGlobalInstallBin();
|
||||
return;
|
||||
} catch (err) {
|
||||
log(`Failed to copy from AGENT_DESKTOP_BINARY_PATH: ${err.message}`);
|
||||
}
|
||||
} else {
|
||||
log(`AGENT_DESKTOP_BINARY_PATH not found: ${customPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (existsSync(binaryPath)) {
|
||||
if (platform() !== 'win32') {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
log(`Native binary ready: ${binaryName}`);
|
||||
await fixGlobalInstallBin();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existsSync(binDir)) {
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
}
|
||||
|
||||
const tarball = `agent-desktop-v${version}-${target}.tar.gz`;
|
||||
const tarballUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${tarball}`;
|
||||
const checksumsUrl = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/checksums.txt`;
|
||||
const tarballPath = join(binDir, tarball);
|
||||
const checksumsPath = join(binDir, 'checksums.txt');
|
||||
|
||||
log(`Downloading native binary for ${platformKey}...`);
|
||||
|
||||
try {
|
||||
await downloadWithRetry(tarballUrl, tarballPath);
|
||||
log(`Downloaded: ${tarball}`);
|
||||
|
||||
try {
|
||||
await downloadWithRetry(checksumsUrl, checksumsPath);
|
||||
const checksums = readFileContent(checksumsPath);
|
||||
const expectedLine = checksums.split('\n').find((line) => line.includes(tarball));
|
||||
if (expectedLine) {
|
||||
const expectedHash = expectedLine.split(/\s+/)[0];
|
||||
if (!verifyChecksum(tarballPath, expectedHash)) {
|
||||
log('WARNING: Checksum verification failed. Binary may be corrupted.');
|
||||
log('Try reinstalling: npm install -g agent-desktop');
|
||||
unlinkSync(tarballPath);
|
||||
unlinkSync(checksumsPath);
|
||||
return;
|
||||
}
|
||||
log('Checksum verified');
|
||||
}
|
||||
unlinkSync(checksumsPath);
|
||||
} catch (err) {
|
||||
log(`Could not verify checksum: ${err.message}`);
|
||||
}
|
||||
|
||||
execSync(`tar -xzf "${tarballPath}" -C "${binDir}"`, { stdio: 'pipe' });
|
||||
|
||||
const extractedBinary = join(binDir, 'agent-desktop');
|
||||
if (existsSync(extractedBinary) && extractedBinary !== binaryPath) {
|
||||
renameSync(extractedBinary, binaryPath);
|
||||
}
|
||||
|
||||
if (platform() !== 'win32') {
|
||||
chmodSync(binaryPath, 0o755);
|
||||
}
|
||||
|
||||
unlinkSync(tarballPath);
|
||||
log(`Installed native binary: ${binaryName}`);
|
||||
} catch (err) {
|
||||
log(`Could not download native binary: ${err.message}`);
|
||||
log('');
|
||||
log('You can download manually from:');
|
||||
log(` ${tarballUrl}`);
|
||||
log('');
|
||||
log(`Then place the binary at: ${binaryPath}`);
|
||||
|
||||
try {
|
||||
if (existsSync(tarballPath)) unlinkSync(tarballPath);
|
||||
if (existsSync(checksumsPath)) unlinkSync(checksumsPath);
|
||||
} catch {}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await fixGlobalInstallBin();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log(`Postinstall error: ${err.message}`);
|
||||
process.exit(0);
|
||||
});
|
||||
29
release-please-config.json
Normal file
29
release-please-config.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
||||
"release-type": "rust",
|
||||
"bump-minor-pre-major": true,
|
||||
"bump-patch-for-minor-pre-major": true,
|
||||
"packages": {
|
||||
".": {
|
||||
"package-name": "agent-desktop",
|
||||
"changelog-path": "CHANGELOG.md",
|
||||
"extra-files": [
|
||||
"npm/package.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"cargo-workspace"
|
||||
],
|
||||
"changelog-sections": [
|
||||
{ "type": "feat", "section": "Features" },
|
||||
{ "type": "fix", "section": "Bug Fixes" },
|
||||
{ "type": "perf", "section": "Performance" },
|
||||
{ "type": "refactor", "section": "Refactoring", "hidden": true },
|
||||
{ "type": "docs", "section": "Documentation", "hidden": true },
|
||||
{ "type": "style", "hidden": true },
|
||||
{ "type": "chore", "hidden": true },
|
||||
{ "type": "ci", "hidden": true },
|
||||
{ "type": "test", "hidden": true }
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue