# How earendil π Detects Bun Self-Updates: Runtime Environment and Install Method Checks

> Learn how earendil pi detects bun self-updates by checking its runtime environment and install method. Understand Bun's self-update detection process.

- Repository: [Earendil Works/pi](https://github.com/earendil-works/pi)
- Tags: how-to-guide
- Published: 2026-05-25

---

**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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/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.

```typescript
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.

```typescript
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.

```typescript
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:

```typescript
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`](https://github.com/earendil-works/pi/blob/main/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`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/test/config.test.ts).

## Summary

- **Virtual filesystem detection**: `isBunBinary` identifies compiled bun binaries by scanning `import.meta.url` for `$bunfs`, `~BUN`, or `%7EBUN` markers.
- **Runtime version checking**: `isBunRuntime` confirms Bun execution via `process.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-scripts` commands, 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.