Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ The agent will also be available as a Claude Code plugin.

- `NATIVEAPPTEMPLATE_VISUAL=1` — opts the run into Stage 1 visual judging (Layer 3). When set, Layer 2 runs in **build mode** instead of fast mode (full `xcodebuild build` + `./gradlew assembleDebug`), then for each platform the agent installs the app on the booted sim/emulator, captures the home screen, and judges it with Opus 4.7 vision against `DEFAULT_STAGE1_RUBRIC`. Adds 60-180s per platform depending on cold-build time. Requires a sim/emulator booted for each platform you want judged. Off by default — `npm run dev` keeps the existing fast path.
- `NATIVEAPPTEMPLATE_AGENT_ANTHROPIC_KEY` — dedicated workspace key, see [Security](#security).
- `ANDROID_SERIAL` — when more than one Android device/emulator is attached (e.g. a physical device plus a running emulator), `adb` standard practice is to set `ANDROID_SERIAL=<serial>` to disambiguate. The agent honors this transparently because it runs `adb` directly. Run `adb devices` to list serials. Visual-judge runs with multiple Android targets attached will error with `more than one device/emulator` if this isn't set.

The agent resolves `adb` to a known-good binary in this priority order: `$ANDROID_HOME/platform-tools/adb`, `$ANDROID_SDK_ROOT/platform-tools/adb`, `~/Library/Android/sdk/platform-tools/adb` (Android Studio default), `/Applications/android-sdk-macosx/platform-tools/adb`, `/opt/homebrew/bin/adb`, `/usr/local/bin/adb`, then PATH. This avoids surprises like a stale `~/.apportable/SDK/bin/adb` (i386, won't exec on Apple Silicon) shadowing a working `adb` on PATH.

## Validation (three layers)

Expand Down
30 changes: 30 additions & 0 deletions src/adb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

// Some dev machines have a stale `adb` shadowing the modern one on PATH —
// the canonical case is /Users/<u>/.apportable/SDK/bin/adb, an i386 binary
// from 2014 that fails to exec on Apple Silicon with "Unknown system error
// -86". Resolve to a known-good adb instead of trusting PATH order:
//
// 1. $ANDROID_HOME/platform-tools/adb (canonical env var)
// 2. $ANDROID_SDK_ROOT/platform-tools/adb (legacy spelling)
// 3. ~/Library/Android/sdk/platform-tools/adb (Android Studio default on macOS)
// 4. /Applications/android-sdk-macosx/platform-tools/adb (older macOS standalone install)
// 5. /opt/homebrew/bin/adb (Homebrew on Apple Silicon)
// 6. /usr/local/bin/adb (Homebrew on Intel)
// 7. "adb" (fall back to PATH lookup)
export function resolveAdbPath(): string {
const candidates = [
process.env['ANDROID_HOME'] ? join(process.env['ANDROID_HOME'], "platform-tools", "adb") : null,
process.env['ANDROID_SDK_ROOT'] ? join(process.env['ANDROID_SDK_ROOT'], "platform-tools", "adb") : null,
join(homedir(), "Library", "Android", "sdk", "platform-tools", "adb"),
"/Applications/android-sdk-macosx/platform-tools/adb",
"/opt/homebrew/bin/adb",
"/usr/local/bin/adb",
];
for (const candidate of candidates) {
if (candidate && existsSync(candidate)) return candidate;
}
return "adb";
}
6 changes: 4 additions & 2 deletions src/validation/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createWriteStream } from "node:fs";
import { mkdir } from "node:fs/promises";
import { dirname } from "node:path";
import { scrubbedEnv } from "../env.js";
import { resolveAdbPath } from "../adb.js";

export type CapturePlatform = "ios" | "android";

Expand Down Expand Up @@ -97,12 +98,13 @@ async function captureIos(outPath: string, timeoutMs: number): Promise<CaptureRe
}

async function captureAndroid(outPath: string, timeoutMs: number): Promise<CaptureResult> {
const command = `adb exec-out screencap -p > ${outPath}`;
const adb = resolveAdbPath();
const command = `${adb} exec-out screencap -p > ${outPath}`;
const started = Date.now();
return new Promise((resolvePromise) => {
let child;
try {
child = spawn("adb", ["exec-out", "screencap", "-p"], {
child = spawn(adb, ["exec-out", "screencap", "-p"], {
env: scrubbedEnv(),
stdio: ["ignore", "pipe", "pipe"],
});
Expand Down
10 changes: 6 additions & 4 deletions src/validation/launch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { spawn } from "node:child_process";
import { scrubbedEnv } from "../env.js";
import { resolveAdbPath } from "../adb.js";

export type LaunchResult = {
ok: boolean;
Expand Down Expand Up @@ -78,11 +79,12 @@ async function installAndLaunchIos(appPath: string, bundleId: string, timeoutMs:
}

async function installAndLaunchAndroid(apkPath: string, packageName: string, timeoutMs: number): Promise<LaunchResult> {
const adb = resolveAdbPath();
const started = Date.now();
const installCmd = `adb install -r ${apkPath}`;
const launchCmd = `adb shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`;
const installCmd = `${adb} install -r ${apkPath}`;
const launchCmd = `${adb} shell monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`;

const install = await runOnce("adb", ["install", "-r", apkPath], timeoutMs);
const install = await runOnce(adb, ["install", "-r", apkPath], timeoutMs);
if (!install.ok) {
return {
ok: false,
Expand All @@ -92,7 +94,7 @@ async function installAndLaunchAndroid(apkPath: string, packageName: string, tim
};
}
const launch = await runOnce(
"adb",
adb,
["shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1"],
timeoutMs,
);
Expand Down
Loading