How earendil π Detects Bun Self-Updates: Runtime Environment and Install Method Checks
earendil π detects bun self-updates by analyzing import.meta.url for virtual filesystem markers, checking process.versions.bun, and inspecting the install path for Bun's global directory, then constructs bun install -g commands in packages/coding-agent/src/config.ts.
The earendil π coding agent supports seamless self-updates when installed via the Bun JavaScript runtime. According to the source code in the earendil-works/pi repository, the detection logic relies on a multi-layered approach that distinguishes between compiled bun binaries, runtime environments, and global installs to determine the correct update strategy.
Detecting Bun Runtime and Binary Environments
The system differentiates between compiled binaries and runtime environments through two distinct checks defined in packages/coding-agent/src/config.ts.
Detecting Compiled Bun Binaries
isBunBinary returns true when the program executes from a compiled bun binary. This boolean check inspects import.meta.url for virtual filesystem markers that Bun uses internally: $bunfs, ~BUN, or the URL-encoded variant %7EBUN. These strings appear in the virtual filesystem path when running standalone executables packaged by Bun.
Detecting the Bun Runtime
isBunRuntime returns true when the current Node process reports Bun in its version object. The implementation checks for the existence of process.versions.bun, which Bun populates when it executes JavaScript code. This distinguishes scripts running under bun from those running under Node.js.
Identifying the Global Bun Install Method
Once runtime detection completes, detectInstallMethod() determines how the package was originally installed by analyzing the execution path and runtime flags.
Path Resolution and Signature Matching
The function constructs a resolved path by combining __dirname with process.execPath, normalizes it to lowercase, and converts Windows backslashes to forward slashes. For Bun global installations, the code specifically searches for the characteristic path segment /install/global/node_modules/, which indicates Bun's global package directory structure.
Install Method Classification
The function returns "bun-binary" immediately if isBunBinary evaluates to true. Otherwise, if isBunRuntime is true or the resolved path contains the Bun global directory signature, it returns "bun". This differentiation allows the system to handle standalone binaries differently from package manager installations.
export function detectInstallMethod(): InstallMethod {
if (isBunBinary) {
return "bun-binary";
}
const resolvedPath = `${__dirname}\0${process.execPath || ""}`
.toLowerCase()
.replace(/\\/g, "/");
// ... npm/pnpm/yarn checks omitted ...
if (isBunRuntime || resolvedPath.includes("/install/global/node_modules/")) {
return "bun";
}
// fallback ...
}
Building the Self-Update Command
When the detected method is "bun", getSelfUpdateCommandForMethod() constructs the appropriate shell commands. The generated command uses bun install -g --ignore-scripts to update the package, optionally preceded by bun uninstall -g if the update package name differs from the currently installed package name.
case "bun":
return makeSelfUpdateCommand(
makeSelfUpdateCommandStep("bun", ["install", "-g", "--ignore-scripts", updatePackageName]),
updatePackageName === installedPackageName
? undefined
: makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]),
);
The --ignore-scripts flag prevents post-install scripts from running during the update, ensuring a clean replacement of the binary without interference from lifecycle hooks.
The Complete Self-Update Flow
The getSelfUpdateCommand() function orchestrates the entire detection and command generation process. It first calls detectInstallMethod(), then delegates to getSelfUpdateCommandForMethod() to build the command string, and finally validates that the environment supports self-updates.
export function getSelfUpdateCommand(
packageName: string,
npmCommand?: string[],
updatePackageName = packageName,
): SelfUpdateCommand | undefined {
const method = detectInstallMethod();
const command = getSelfUpdateCommandForMethod(
method, packageName, updatePackageName, npmCommand);
if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) ||
!isSelfUpdatePathWritable()) {
return undefined;
}
return command;
}
The function returns undefined if validation fails, including checks via isManagedByGlobalPackageManager() (confirming global package manager control) and isSelfUpdatePathWritable() (verifying write permissions to the installation directory).
Implementation Example
To utilize the self-update detection in code interacting with earendil π, import the configuration utilities and inspect the generated commands:
import { detectInstallMethod, getSelfUpdateCommand } from "@earendil-works/pi-coding-agent/src/config";
// 1. Detect the current install method (returns "bun" for global bun installs)
const method = detectInstallMethod();
console.log("Install method:", method);
// 2. Build a self-update command for the package
const updateCmd = getSelfUpdateCommand("@earendil-works/pi-coding-agent");
if (updateCmd) {
console.log("Run:", updateCmd.display);
// Output: bun uninstall -g @earendil-works/pi-coding-agent && bun install -g --ignore-scripts @earendil-works/pi-coding-agent
} else {
console.log("Self-update not possible in this environment.");
}
The CLI entry point in packages/coding-agent/src/package-manager-cli.ts invokes these functions when processing the self-update subcommand, as verified by the test suite in packages/coding-agent/test/config.test.ts.
Summary
- Virtual filesystem detection:
isBunBinaryidentifies compiled bun binaries by scanningimport.meta.urlfor$bunfs,~BUN, or%7EBUNmarkers. - Runtime version checking:
isBunRuntimeconfirms Bun execution viaprocess.versions.bun. - Install path analysis:
detectInstallMethod()identifies global bun installations by the presence of/install/global/node_modules/in the resolved path. - Command construction: The system generates
bun install -g --ignore-scriptscommands, with preceding uninstall steps when package names differ. - Safety validation:
getSelfUpdateCommand()verifies global package manager management and directory write permissions before returning executable commands.
Frequently Asked Questions
How does earendil π distinguish between a compiled bun binary and the bun runtime?
isBunBinary checks import.meta.url for virtual filesystem paths containing $bunfs, ~BUN, or %7EBUN, which indicate a compiled standalone binary. isBunRuntime checks process.versions.bun to detect when code runs under the Bun JavaScript runtime. A compiled binary may trigger isBunBinary while running in contexts where isBunRuntime behaves differently, allowing the system to handle compiled executables separately from runtime-installed packages.
What path signature indicates a global bun installation in earendil π?
The detectInstallMethod() function identifies global bun installations when the normalized, lowercase resolved path contains /install/global/node_modules/. This directory structure is Bun's standard location for globally installed packages, and the check runs after combining __dirname with process.execPath and converting Windows backslashes to forward slashes.
Why does the generated self-update command use the --ignore-scripts flag?
The --ignore-scripts flag prevents post-installation lifecycle scripts from executing during the update process. This ensures that the replacement of the earendil π binary occurs cleanly without interference from hooks that might attempt to access the very files being replaced, avoiding potential file-locking issues or partial updates during global package replacement.
What prevents earendil π from generating self-update commands in certain environments?
getSelfUpdateCommand() returns undefined when three conditions are not satisfied: the install method must be successfully detected, isManagedByGlobalPackageManager() must confirm the package is under global package manager control, and isSelfUpdatePathWritable() must verify that the installation directory is writable by the current process. These checks prevent update failures in read-only environments, local development installs, or when running from source code rather than a managed global package installation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →