Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

extensions/klaus/src/runtime/android.ts

Raw
import { execFile } from "node:child_process";
import { chmod, mkdir, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { dbg, diagnosticKind } from "../debug.js";
import { findExecutable } from "../pi-ext-executable.ts";

const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const PACKAGE = "@anthropic-ai/claude-agent-sdk-linux-arm64";

let resolution: Promise<string> | undefined;

function shellQuote(value: string): string {
	return `'${value.replaceAll("'", `'"'"'`)}'`;
}

// Android is bionic, so the glibc arm64 claude binary runs through glibc-runner
// (grun). The binary itself must already be installed (npm skips the SDK's
// `os: linux` packages on an `android` platform, so it is not auto-resolved);
// this runtime only locates it and writes a Termux-safe grun launcher.
async function prepare(): Promise<string> {
	dbg?.("runtime.android.prepare.start");
	if (process.arch !== "arm64") {
		throw new Error(`Klaus does not support Android ${process.arch}.`);
	}
	const grun = findExecutable(["grun"]);
	if (!grun) {
		throw new Error(
			"Klaus requires glibc-runner on Android: pkg install glibc-repo && pkg install glibc-runner",
		);
	}
	let packageJson: string;
	try {
		packageJson = require.resolve(`${PACKAGE}/package.json`);
	} catch {
		throw new Error(
			`Klaus runtime ${PACKAGE} is not installed; install it alongside klaus so grun can launch it.`,
		);
	}
	const claude = join(dirname(packageJson), "claude");
	const root = join(
		process.env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state"),
		"pi",
		"klaus",
		"runtime",
	);
	await mkdir(root, { recursive: true, mode: 0o700 });
	const launcher = join(root, "claude-klaus");
	await writeFile(
		launcher,
		`#!/data/data/com.termux/files/usr/bin/sh\nexec ${shellQuote(grun)} ${shellQuote(claude)} "$@"\n`,
		{ mode: 0o700 },
	);
	await chmod(launcher, 0o700);
	await execFileAsync(launcher, ["--version"], {
		timeout: 30_000,
		maxBuffer: 64 * 1024,
	});
	return launcher;
}

export function resolveAndroidRuntime(): Promise<string> {
	dbg?.("runtime.android.resolve", { cached: Boolean(resolution) });
	resolution ??= prepare().catch((error) => {
		dbg?.("runtime.android.resolve.error", { kind: diagnosticKind(error) });
		resolution = undefined;
		throw error;
	});
	return resolution;
}