Install the Rust CLI as `oa` only, and say when `oa` is shadowed

467a98801873 · AtlantisPleb · · parent 630bcd49f79e

Install the Rust CLI as `oa` only, and say when `oa` is shadowed

The installer took the `openagents` name: it linked `~/.openagents/bin/openagents`
at the Rust binary and symlinked that onto PATH, ahead of the TypeScript CLI
installed under nvm. Scripts, agent tool calls, and the coder's own `openagents`
tool all invoke that name, and the Rust build implements a strict subset of the
commands, so `openagents issue reopen` began failing with `unrecognized
subcommand` in workflows that had always worked. Taking the name is a claim of
parity the binary cannot make yet.

It now installs `oa` and nothing else, and removes an `openagents` link left by
an earlier run -- but only where the link points into our own download
directory, so an `openagents` that resolves anywhere else is never touched.

The installer also now reports when `oa` will not run what it just wrote. Two
cases, both of which it can see and neither of which it could before:

  - another `oa` earlier on PATH, found by comparing the resolved path against
    the installed one;
  - an alias or function named `oa` in the shell rc file. That one is the
    reason this check exists. Aliases and functions beat PATH in an interactive
    shell, the installer runs non-interactively so `command -v` cannot see
    them, and no output from either binary says which one answered -- so the
    symptom is a CLI that reports a version it is not running. Grepping the rc
    file is the only way to catch it from inside the installer.

It reports and never edits: the alias may be deliberate, and rewriting someone's
shell config silently is worse than the shadowing it would fix.

Refs openagents#90.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 436 · 2026-08-26T06:38:12.975857Z

Changed files

  • modified priv/static/install.sh
  • modified test/openagents_web/install_script_test.exs

Diff

2 files changed, +96 -11

priv/static/install.sh modified +87 -10

@@ -303,13 +303,39 @@ fi

303 303
rm -f "$sums_tmp"
304 304
echo "  Verified sha256 ${actual}." >&2
305 305
306
# This binary installs as `oa` and never as `openagents`.
307
#
308
# `openagents` is the TypeScript CLI, and it is the one scripts, agent tool
309
# calls, and the coder's own `openagents` tool invoke by name. This binary
310
# implements a strict subset of its commands, so taking that name silently
311
# swaps a fraction of the CLI in underneath everything that calls it, and the
312
# failure surfaces far from the cause -- `unrecognized subcommand 'reopen'`
313
# from a script that has always worked. Taking the name is a parity claim, and
314
# it is not one this binary can make yet. See openagents#90.
315
#
316
# A previous version of this installer did take it. Undo that here, and only
317
# where the link points into our own install -- never touch an `openagents`
318
# that resolves to something we did not put there.
319
reclaim_openagents_name() {
320
    for dir in "$BIN_DIR" "$HOME/.local/bin" "/usr/local/bin"; do
321
        link="$dir/openagents"
322
        [ -L "$link" ] || continue
323
        target="$(readlink "$link" 2>/dev/null || true)"
324
        case "$target" in
325
            *"$(basename "$DOWNLOAD_DIR")"/openagents-*|"$BIN_DIR"/oa|"$BIN_DIR"/openagents|../*/openagents-*)
326
                rm -f "$link" 2>/dev/null || true
327
                echo "  Removed $link, which shadowed the openagents CLI." >&2
328
                ;;
329
        esac
330
    done
331
}
332
306 333
if [ "$os" = "windows" ]; then
307 334
    mv -f "$binary_tmp" "$binary_path"
308
    for bin_name in openagents.exe oa.exe; do
309
        rm -f "$BIN_DIR/$bin_name.old" 2>/dev/null || true
310
        cp -f "$binary_path" "$BIN_DIR/$bin_name" 2>/dev/null || true
311
    done
312
    echo "  Binary installed to $BIN_DIR/openagents.exe and $BIN_DIR/oa.exe." >&2
335
    rm -f "$BIN_DIR/oa.exe.old" 2>/dev/null || true
336
    cp -f "$binary_path" "$BIN_DIR/oa.exe" 2>/dev/null || true
337
    rm -f "$BIN_DIR/openagents.exe" 2>/dev/null || true
338
    echo "  Binary installed to $BIN_DIR/oa.exe." >&2
313 339
else
314 340
    chmod +x "$binary_tmp"
315 341
    mv -f "$binary_tmp" "$binary_path"

@@ -319,9 +345,9 @@ else

319 345
    else
320 346
        link_target="$binary_path"
321 347
    fi
322
    ln -sf "$link_target" "$BIN_DIR/openagents"
323 348
    ln -sf "$link_target" "$BIN_DIR/oa"
324
    echo "  Binary linked to $BIN_DIR/openagents and $BIN_DIR/oa." >&2
349
    reclaim_openagents_name
350
    echo "  Binary linked to $BIN_DIR/oa." >&2
325 351
fi
326 352
327 353
path_has_dir() {

@@ -332,10 +358,8 @@ SYMLINK_CREATED=""

332 358
if [ "$os" != "windows" ] && ! path_has_dir "$BIN_DIR"; then
333 359
    for candidate in "$HOME/.local/bin" "/usr/local/bin"; do
334 360
        if path_has_dir "$candidate" && [ -d "$candidate" ] && [ -w "$candidate" ]; then
335
            ln -sf "$BIN_DIR/openagents" "$candidate/openagents"
336 361
            ln -sf "$BIN_DIR/oa" "$candidate/oa"
337 362
            SYMLINK_CREATED="$candidate"
338
            echo "  Symlinked $candidate/openagents -> $BIN_DIR/openagents" >&2
339 363
            echo "  Symlinked $candidate/oa -> $BIN_DIR/oa" >&2
340 364
            break
341 365
        fi

@@ -379,6 +403,59 @@ export PATH="$HOME/.openagents/bin:$PATH"

379 403
    echo "  Updated $BIN_DIR in PATH in $config_file." >&2
380 404
fi
381 405
406
# Tell people when `oa` will not run what we just installed.
407
#
408
# Two ways that happens, and the installer can see both:
409
#
410
#   1. Another `oa` sits earlier on PATH.
411
#   2. A shell alias or function named `oa` is defined in the rc file. This one
412
#      is nastier: aliases and functions beat PATH in an interactive shell, the
413
#      installer runs non-interactively so `command -v oa` cannot see them, and
414
#      nothing about the running binary says which one answered. Grepping the
415
#      rc file is the only way to catch it from here.
416
#
417
# Report, never edit. The alias may be deliberate, and silently rewriting
418
# someone's shell config is worse than the shadowing.
419
CONFLICTS=""
420
421
installed_oa="$BIN_DIR/oa"
422
[ "$os" = "windows" ] && installed_oa="$BIN_DIR/oa.exe"
423
424
resolved_oa="$(command -v oa 2>/dev/null || true)"
425
if [ -n "$resolved_oa" ]; then
426
    resolved_real="$(cd "$(dirname "$resolved_oa")" 2>/dev/null && pwd -P)/$(basename "$resolved_oa")"
427
    installed_real="$(cd "$(dirname "$installed_oa")" 2>/dev/null && pwd -P)/$(basename "$installed_oa")"
428
    if [ -n "$SYMLINK_CREATED" ] && [ "$resolved_real" = "$SYMLINK_CREATED/oa" ]; then
429
        : # our own symlink, which points at the binary we just installed
430
    elif [ "$resolved_real" != "$installed_real" ]; then
431
        CONFLICTS="yes"
432
        echo "" >&2
433
        echo "WARNING: 'oa' on your PATH is not what this installer just wrote." >&2
434
        echo "  PATH resolves oa to: $resolved_oa" >&2
435
        echo "  Installed here:      $installed_oa" >&2
436
    fi
437
fi
438
439
if [ -n "$config_file" ] && [ -f "$config_file" ]; then
440
    if grep -Eqs '^[[:space:]]*(alias[[:space:]]+oa=|oa[[:space:]]*\(\)|function[[:space:]]+oa\b)' "$config_file"; then
441
        CONFLICTS="yes"
442
        echo "" >&2
443
        echo "WARNING: $config_file defines a shell alias or function named 'oa'." >&2
444
        echo "  It takes precedence over PATH, so typing 'oa' will not run the" >&2
445
        echo "  binary this installer wrote, and nothing in the output will say so." >&2
446
        echo "  Remove it, rename it, or run $installed_oa by its full path." >&2
447
        grep -Ens '^[[:space:]]*(alias[[:space:]]+oa=|oa[[:space:]]*\(\)|function[[:space:]]+oa\b)' "$config_file" \
448
            | sed 's/^/    /' >&2
449
    fi
450
fi
451
382 452
echo "" >&2
383 453
echo "OpenAgents CLI $version installation complete!" >&2
384
echo "Run 'openagents' or 'oa' to get started." >&2
454
if [ -n "$CONFLICTS" ]; then
455
    echo "Run '$installed_oa --version' to confirm which build answers." >&2
456
else
457
    echo "Run 'oa' to get started." >&2
458
fi
459
echo "" >&2
460
echo "Note: this installs 'oa' only. The 'openagents' command is the separate" >&2
461
echo "TypeScript CLI (npm i -g @openagentsinc/cli) and is left alone." >&2
test/openagents_web/install_script_test.exs modified +9 -1

@@ -61,7 +61,15 @@ defmodule OpenAgentsWeb.InstallScriptTest do

61 61
    assert {_output, 0} = System.cmd("sh", ["-n", @script], stderr_to_stdout: true)
62 62
63 63
    script = File.read!(@script)
64
    body = String.replace(script, ~r/^#.*$/m, "")
64
65
    body =
66
      script
67
      |> String.replace(~r/^#.*$/m, "")
68
      # `[[:space:]]` and its siblings are POSIX character classes. They are
69
      # valid in `sh` and in POSIX `grep -E`, busybox included, and they
70
      # contain `[[` without being a bash conditional. Drop them before
71
      # looking for one, or the check below fails on correct POSIX code.
72
      |> String.replace(~r/\[\[:[a-z]+:\]\]/, "")
65 73
66 74
    refute body =~ "[[", "`[[ ]]` is a bash conditional"
67 75
    refute body =~ "=~", "`=~` is a bash regex match"

This page updates live while a promote is in flight · changelog