repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/klaus/src/runtime/android.ts
Rawimport { 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;
}