diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 14663da..af78c93 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -177,7 +177,7 @@ factory_desktop_packages: - iproute2 # --- Tool installer --------------------------------------------------------- -factory_core_tools: [herdr, node, bun, uv] +factory_core_tools: [herdr, node, bun, uv, btop] # The installer flags that choose which tools a run covers. tasks/preflight.yml # resolves exactly this set (plus the sources Ansible installs itself) and # tasks/tools.yml installs it, so a host never resolves, and never fails on, a @@ -210,6 +210,10 @@ factory_no_mistakes_omp_overlay: "{{ factory_cfg.home }}/.no-mistakes/omp-config # Stdlib-only reporter that feeds the Spaces sidebar layout, run by herdr-spaces.timer. factory_herdr_spaces_source: "{{ code_factory_repo }}/maintenance/herdr-spaces.py" factory_herdr_spaces_script: "{{ factory_local_bin }}/herdr-spaces.py" +# btop: ~/.local/bin/btop is a launcher that picks a preset of the saved config +# by pane size, so it also loads in a phone's 50-column pane (docs/herdr.md#btop). +factory_btop_launcher_source: "{{ code_factory_repo }}/maintenance/btop.sh" +factory_btop_config: "{{ factory_cfg.home }}/.config/btop/btop.conf" # --- Browser pruning -------------------------------------------------------- # Flags confirmed by Main (owner of maintenance/chrome-autoprune.py): without diff --git a/ansible/tasks/herdr.yml b/ansible/tasks/herdr.yml index 0c6b02e..3d44847 100644 --- a/ansible/tasks/herdr.yml +++ b/ansible/tasks/herdr.yml @@ -105,6 +105,38 @@ remote_src: true become: true +# btop fitted to its pane (docs/herdr.md#btop): the launcher takes the place of +# the btop command and runs the installer's btop-bin with a preset of this +# config. btop rewrites its config on exit; every apply puts the saved one back. +- name: Install the btop launcher + ansible.builtin.copy: + src: "{{ factory_btop_launcher_source }}" + dest: "{{ factory_local_bin }}/btop" + owner: "{{ factory_cfg.user }}" + group: "{{ factory_group }}" + mode: "0755" + remote_src: true + become: true + +- name: Ensure the btop configuration directory + ansible.builtin.file: + path: "{{ factory_btop_config | dirname }}" + state: directory + owner: "{{ factory_cfg.user }}" + group: "{{ factory_group }}" + mode: "0755" + become: true + +- name: Install the btop configuration + ansible.builtin.copy: + src: "{{ code_factory_repo }}/config/btop.conf" + dest: "{{ factory_btop_config }}" + owner: "{{ factory_cfg.user }}" + group: "{{ factory_group }}" + mode: "0644" + remote_src: true + become: true + - name: Render the Herdr Spaces reporter unit and timer ansible.builtin.template: src: "{{ item }}.j2" diff --git a/ansible/tasks/tools.yml b/ansible/tasks/tools.yml index 6109895..8eff520 100644 --- a/ansible/tasks/tools.yml +++ b/ansible/tasks/tools.yml @@ -3,7 +3,7 @@ # latest releases tasks/preflight.yml resolved. # # Contract: python3 /scripts/install_tools.py -# --home --tools herdr,node,bun,uv [--npm] [--development] +# --home --tools herdr,node,bun,uv,btop [--npm] [--development] # --resolved # The installer is idempotent and prints a final JSON object # {"changed": bool, "installed": [...]}, which is the only changed signal this diff --git a/ansible/tasks/verify.yml b/ansible/tasks/verify.yml index c21bea2..504908a 100644 --- a/ansible/tasks/verify.yml +++ b/ansible/tasks/verify.yml @@ -37,6 +37,9 @@ factory_user_units ~ '/default.target.wants/herdr.service', factory_herdr_spaces_script, factory_local_bin ~ '/herdr-sidebar-to-client.py', + factory_local_bin ~ '/btop', + factory_local_bin ~ '/btop-bin', + factory_btop_config, factory_user_units ~ '/herdr-spaces.service', factory_user_units ~ '/herdr-spaces.timer', factory_user_units ~ '/timers.target.wants/herdr-spaces.timer', diff --git a/config/btop.conf b/config/btop.conf new file mode 100644 index 0000000..e3ac198 --- /dev/null +++ b/config/btop.conf @@ -0,0 +1,254 @@ +#? Config file for btop v.1.4.7 + +#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. +#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" +color_theme = "Default" + +#* If the theme set background should be shown, set to False if you want terminal background transparency. +theme_background = false + +#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. +truecolor = true + +#* Set to true to force tty mode regardless if a real tty has been detected or not. +#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols. +force_tty = false + +#* Option to disable presets. Either the default preset, custom presets, or all presets. +#* "Off" All presets are enabled. +#* "Default" preset is disabled.#* "Custom" presets are disabled.#* "All" presets are disabled. +disable_presets = "Off" + +#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets. +#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. +#* Use whitespace " " as separator between different presets. +#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty" +presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty proc:0:default" + +#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists. +#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. +vim_keys = false + +#* Disable all mouse events. +disable_mouse = false + +#* Rounded corners on boxes, is ignored if TTY mode is ON. +rounded_corners = true + +#* Use terminal synchronized output sequences to reduce flickering on supported terminals. +terminal_sync = true + +#* Default symbols to use for graph creation, "braille", "block" or "tty". +#* "braille" offers the highest resolution but might not be included in all fonts. +#* "block" has half the resolution of braille but uses more common characters. +#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY. +#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view. +graph_symbol = "braille" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_cpu = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_mem = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_net = "default" + +# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". +graph_symbol_proc = "default" + +#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace. +shown_boxes = "cpu mem net proc" + +#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs. +update_ms = 100 + +#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct", +#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly. +proc_sorting = "cpu direct" + +#* Reverse sorting order, True or False. +proc_reversed = false + +#* Show processes as a tree. +proc_tree = false + +#* Use the cpu graph colors in the process list. +proc_colors = true + +#* Use a darkening gradient in the process list. +proc_gradient = true + +#* If process cpu usage should be of the core it's running on or usage of the total available cpu power. +proc_per_core = false + +#* Show process memory as bytes instead of percent. +proc_mem_bytes = true + +#* Show cpu graph for each process. +proc_cpu_graphs = true + +#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate) +proc_info_smaps = false + +#* Show proc box on left side of screen instead of right. +proc_left = false + +#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop). +proc_filter_kernel = false + +#* Should the process list follow the selected process when detailed view is open. +proc_follow_detailed = true + +#* In tree-view, always accumulate child process resources in the parent process. +proc_aggregate = false + +#* In tree-view, auto-collapse processes with this many or more direct children when +#* entering tree mode. 0 to disable. Useful for collapsing multi-process apps like browsers. +proc_tree_auto_collapse = 0 + +#* Should cpu and memory usage display be preserved for dead processes when paused. +keep_dead_proc_usage = false + +#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_upper = "Auto" + +#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available. +#* Select from a list of detected attributes from the options menu. +cpu_graph_lower = "Auto" + +#* Toggles if the lower CPU graph should be inverted. +cpu_invert_lower = true + +#* Set to True to completely disable the lower CPU graph. +cpu_single_graph = false + +#* Show cpu box at bottom of screen instead of top. +cpu_bottom = false + +#* Shows the system uptime in the CPU box. +show_uptime = true + +#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo. +show_cpu_watts = true + +#* Show cpu temperature. +check_temp = true + +#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors. +cpu_sensor = "Auto" + +#* Show temperatures for cpu cores also if check_temp is True and sensors has been found. +show_coretemp = true + +#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core. +#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine. +#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries. +#* Example: "4:0 5:1 6:3" +cpu_core_map = "" + +#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine". +temp_scale = "celsius" + +#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024. +base_10_sizes = false + +#* Show CPU frequency. +show_cpu_freq = true + +#* How to calculate CPU frequency, available values: "first", "range", "lowest", "highest" and "average". +freq_mode = "first" + +#* Draw a clock at top of screen, formatting according to strftime, empty string to disable. +#* Special formatting: /host = hostname | /user = username | /uptime = system uptime +clock_format = "%X" + +#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort. +background_update = true + +#* Custom cpu model name, empty string to disable. +custom_cpu_name = "" + +#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ". +#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disks_filter="/boot /home/user", disks_filter="exclude=/boot /home/user" +disks_filter = "" + +#* Show graphs instead of meters for memory values. +mem_graphs = true + +#* Show mem box below net box instead of above. +mem_below_net = false + +#* Count ZFS ARC in cached and available memory. +zfs_arc_cached = true + +#* If swap memory should be shown in memory box. +show_swap = true + +#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk. +swap_disk = true + +#* If mem box should be split to also show disks info. +show_disks = true + +#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar. +only_physical = true + +#* Read disks list from /etc/fstab. This also disables only_physical. +use_fstab = true + +#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool) +zfs_hide_datasets = false + +#* Set to true to show available disk space for privileged users. +disk_free_priv = false + +#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view. +show_io_stat = true + +#* Toggles io mode for disks, showing big graphs for disk read/write speeds. +io_mode = false + +#* Set to True to show combined read/write io graphs in io mode. +io_graph_combined = false + +#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ". +#* Example: "/mnt/media:100 /:20 /boot:1". +io_graph_speeds = "" + +#* Swap the positions of the upload and download speed graphs. When true, upload will be on top. +swap_upload_download = false + +#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False. +net_download = 100 + +net_upload = 100 + +#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest. +net_auto = true + +#* Sync the auto scaling for download and upload to whichever currently has the highest scale. +net_sync = true + +#* Starts with the Network Interface specified here. +net_iface = "" + +#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes. +base_10_bitrate = "Auto" + +#* Show battery stats in top right if battery is present. +show_battery = true + +#* Which battery to use if multiple are present. "Auto" for auto detection. +selected_battery = "Auto" + +#* Show power stats of battery next to charge indicator. +show_battery_watts = true + +#* Set loglevel for "~/.local/state/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG". +#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. +log_level = "WARNING" + +#* Automatically save current settings to config file on exit. +save_config_on_exit = true diff --git a/docs/dependencies.md b/docs/dependencies.md index 65cf501..521be7d 100644 --- a/docs/dependencies.md +++ b/docs/dependencies.md @@ -13,7 +13,7 @@ Everything the recipe installs, grouped by where it comes from. Nothing is pinne `scripts/install_tools.py`. Native tools are linked into `~/.local/bin`; npm tools are each installed with `npm install` into `~/.local/share/code-factory//` (npm checks the registry integrity) and linked from there. Superseded versions stay on disk. -- Always: herdr ([herdrdev/herdr](https://github.com/herdrdev/herdr/releases/latest)), bun ([oven-sh/bun](https://github.com/oven-sh/bun/releases/latest), the x64 `baseline` build), uv ([astral-sh/uv](https://github.com/astral-sh/uv/releases/latest)), verified against the GitHub release-asset digest. A herdr upgrade rewrites and restarts `herdr.service`. +- Always: herdr ([herdrdev/herdr](https://github.com/herdrdev/herdr/releases/latest)), bun ([oven-sh/bun](https://github.com/oven-sh/bun/releases/latest), the x64 `baseline` build), uv ([astral-sh/uv](https://github.com/astral-sh/uv/releases/latest)), btop ([aristocratos/btop](https://github.com/aristocratos/btop/releases/latest), the static musl build, linked as `btop-bin`; `btop` is the launcher in [btop](herdr.md#btop)), verified against the GitHub release-asset digest. A herdr upgrade rewrites and restarts `herdr.service`. - Always: node, the newest release in the [nodejs.org index](https://nodejs.org/dist/index.json) (not the LTS line), verified against that release's `SHASUMS256.txt`. - `agents` profile, native: gh ([cli/cli](https://github.com/cli/cli/releases/latest)), treehouse ([kunchenguid/treehouse](https://github.com/kunchenguid/treehouse/releases/latest)), verified against the GitHub release-asset digest. - `agents` profile, no-mistakes ([kunchenguid/no-mistakes](https://github.com/kunchenguid/no-mistakes/releases)): the one tool that tracks the prerelease channel. Each apply resolves the newest non-draft release, betas included (not only the latest stable one), and verifies it against the GitHub release-asset digest. @@ -23,7 +23,7 @@ Everything the recipe installs, grouped by where it comes from. Nothing is pinne - `agents` profile, omp plugins: ponytail ([DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail)), i-have-adhd ([ayghri/i-have-adhd](https://github.com/ayghri/i-have-adhd)) and caveman ([JuliusBrussee/caveman](https://github.com/JuliusBrussee/caveman)) from their GitHub marketplaces, installed once and upgraded with `omp plugin upgrade` on every apply. These are the only installs that are not checksum-verified: no publisher posts a checksum for them, so they track each author's default branch and load as agent instructions and hooks. The operator accepted this to keep them at the latest commit. - `development` profile: rustup-init, the version in rustup's [stable release](https://static.rust-lang.org/rustup/release-stable.toml), verified against the `.sha256` published beside it, installing the Rust `stable` toolchain (minimal profile + rustfmt + clippy). Every apply moves the toolchain to the newest stable. -The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); a resolution makes one request per GitHub-hosted tool the host installs, at most eight. Only the tools a run installs are resolved, so a source the host does not use cannot fail it. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply` or `./bootstrap.sh`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. +The GitHub lookups use the GitHub API, which allows 60 unauthenticated requests an hour per IP (shared IPs such as CI runners exhaust it); a resolution makes one request per GitHub-hosted tool the host installs, at most nine. Only the tools a run installs are resolved, so a source the host does not use cannot fail it. The lookups authenticate with `GITHUB_TOKEN` from the environment that runs `./factory apply` or `./bootstrap.sh`, else run unauthenticated; the token is sent to the GitHub API only. Container builds take the token as the optional BuildKit secret `github_token` (`docker build --secret id=github_token,env=GITHUB_TOKEN ...`), so it never lands in the image. ## Ubuntu packages diff --git a/docs/herdr.md b/docs/herdr.md index ef3fb10..5b674c9 100644 --- a/docs/herdr.md +++ b/docs/herdr.md @@ -192,6 +192,14 @@ What the mobile layout forces, compared with the desktop sidebar: - A home's counts and shares are on its own agent's entry, so a home with no agent pane carrying a `who` token shows none. Without a `machine` workspace, the machine's shares do not show either. - The issue (`○`) is left out for width. +### btop + +btop draws nothing in a pane smaller than its shown boxes need; it shows `Terminal size too small` instead. All four boxes need 80x24 in btop 1.4 (the cpu box is 60 columns wide, the proc box 44 beside the 36 of mem and net), so a 50-column phone pane is too small. + +`./factory apply` installs the latest btop release as `~/.local/bin/btop-bin`, [`config/btop.conf`](../config/btop.conf) as `~/.config/btop/btop.conf`, and [`maintenance/btop.sh`](../maintenance/btop.sh) as `~/.local/bin/btop`. The launcher starts btop with preset 0 (all four boxes) in a pane of at least 80x24, and otherwise with preset 4 (processes only, which needs 44x16). When a phone attaches to the same session and the pane crosses 80x24, the launcher restarts btop with the other preset. Quitting btop ends the launcher. + +btop saves its config on exit, including the last preset's boxes; the launcher always passes a preset, so that never changes what it shows. Every apply rewrites the config, so make lasting changes in `config/btop.conf`, and keep preset 4 there. + ## Viewing from another machine Sidebar layouts are client-side: Herdr draws the sidebar from the config of the machine you view from, even over `herdr --remote`. The host still reports every token, but the viewing machine needs the host's layout to show them. `./factory apply` installs [`maintenance/herdr-sidebar-to-client.py`](../maintenance/herdr-sidebar-to-client.py) as `~/.local/bin/herdr-sidebar-to-client.py`. On the viewing machine, run it against the host with Python 3 and ssh access, where `` is anything `ssh` accepts, such as `user@host`: diff --git a/maintenance/btop.sh b/maintenance/btop.sh new file mode 100755 index 0000000..a72bf70 --- /dev/null +++ b/maintenance/btop.sh @@ -0,0 +1,23 @@ +#!/bin/sh +# btop that fits its pane, installed as ~/.local/bin/btop (docs/herdr.md#btop). +# All four boxes need 80x24 in btop 1.4 (cpu 60 wide, proc 44 beside mem/net +# 36); a Herdr phone pane is about 50 columns. Smaller panes get preset 4 from +# btop.conf (processes only, 44x16), others preset 0 (all boxes). A resize that +# crosses 80x24 restarts btop with the other preset. +preset() { + set -- $(stty size) + if [ "$1" -ge 24 ] && [ "$2" -ge 80 ]; then echo 0; else echo 4; fi +} +# No job control here, so a background command reads /dev/null unless given +# stdin explicitly. Not /dev/null; do :; done + [ "$again" ] || exit "$rc" +done diff --git a/scripts/install_tools.py b/scripts/install_tools.py index 14e8fae..f7e4e8a 100755 --- a/scripts/install_tools.py +++ b/scripts/install_tools.py @@ -63,6 +63,14 @@ "uv-{gnu}-unknown-linux-gnu.tar.gz", {"uv": "uv-*/uv", "uvx": "uv-*/uvx"}, ), + # Linked as btop-bin: ~/.local/bin/btop is the launcher that fits it to the + # pane (maintenance/btop.sh, installed by ansible/tasks/herdr.yml). + "btop": ( + "aristocratos/btop", + "v", + "btop-{gnu}-unknown-linux-musl.tar.gz", + {"btop-bin": "btop/bin/btop"}, + ), # Resolved for the fleet browser ladder (ansible/tasks/fleet-browsers.yml), not installed here. "obscura": ( "h4ckf0r0day/obscura", @@ -515,7 +523,7 @@ def omp(*argv): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--home", type=Path, required=True) - parser.add_argument("--tools", default="herdr,node,bun,uv") + parser.add_argument("--tools", default="herdr,node,bun,uv,btop") parser.add_argument("--npm", action="store_true") parser.add_argument("--development", action="store_true") parser.add_argument( diff --git a/tests/container-smoke.sh b/tests/container-smoke.sh index 7fd33e5..1e3224a 100755 --- a/tests/container-smoke.sh +++ b/tests/container-smoke.sh @@ -228,7 +228,7 @@ herdr_installed_bin() { image_is_current() { local latest versions='map_values(.version? // .)' latest=$(python3 "${CF_ROOT}/scripts/install_tools.py" \ - --home "${HOME}" --tools herdr,node,bun,uv --npm --development --resolve) \ + --home "${HOME}" --tools herdr,node,bun,uv,btop --npm --development --resolve) \ || fail "could not resolve the latest releases" [ "$(jq -cS "${versions}" <<<"${latest}")" = "$(jq -cS "${versions}" "${RESOLVED_STAMP}")" ] \ && return 0 @@ -655,7 +655,7 @@ check_installer_idempotent() { local out out=$(python3 "${CF_ROOT}/scripts/install_tools.py" \ --home "${HOME}" \ - --tools herdr,node,bun,uv \ + --tools herdr,node,bun,uv,btop \ --resolved "$(cat "${RESOLVED_STAMP}")" \ --npm --development 2>&1) || { printf '%s\n' "${out}" | tail -n 20 diff --git a/tests/test_install_tools.py b/tests/test_install_tools.py index 1000d5d..6c00fe2 100644 --- a/tests/test_install_tools.py +++ b/tests/test_install_tools.py @@ -105,6 +105,7 @@ def test_download_rejects_plain_http(tmp_path): "kunchenguid/no-mistakes": ("v9.9.10-beta.1", "no-mistakes-v9.9.10-beta.1-linux-amd64.tar.gz"), "kunchenguid/treehouse": ("v9.9.9", "treehouse-v9.9.9-linux-amd64.tar.gz"), "astral-sh/uv": ("9.9.9", "uv-x86_64-unknown-linux-gnu.tar.gz"), + "aristocratos/btop": ("v9.9.9", "btop-x86_64-unknown-linux-musl.tar.gz"), "h4ckf0r0day/obscura": ("v9.9.9", "obscura-x86_64-linux.tar.gz"), "jimididit/koncreet": ("v9.9.9", "koncreet.tar.gz"), } @@ -180,9 +181,19 @@ def release(tag, name, draft=False): def test_latest_releases_are_pinned_to_the_digests_their_publishers_list(monkeypatch): upstream(monkeypatch) latest = installer.resolve_latest("linux-x86_64", EVERYTHING) - for tool in ("herdr", "bun", "gh", "no-mistakes", "treehouse", "uv", "obscura", "koncreet"): + for tool in ( + "herdr", + "bun", + "gh", + "no-mistakes", + "treehouse", + "uv", + "btop", + "obscura", + "koncreet", + ): assert latest[tool]["assets"]["linux-x86_64"]["sha256"] == "a" * 64 - for tool in ("herdr", "bun", "gh", "treehouse", "uv", "obscura", "koncreet"): + for tool in ("herdr", "bun", "gh", "treehouse", "uv", "btop", "obscura", "koncreet"): assert latest[tool]["version"] == "9.9.9" assert latest["gh"]["assets"]["linux-x86_64"]["format"] == "tar" assert latest["bun"]["assets"]["linux-x86_64"]["format"] == "zip"