Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
536 changes: 324 additions & 212 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bn-loader"
version = "0.1.2"
version = "0.2.0"
edition = "2024"
description = "A profile launcher for Binary Ninja that manages multiple configurations"
license = "BSD-3-Clause"
Expand All @@ -24,5 +24,6 @@ serde_json = "1"
toml = "0.9"
globset = "0.4"
termcolor = "1.4"
ureq = "3"
semver = "1"
anyhow = "1"
zip = { version = "2", default-features = false, features = ["deflate"] }
tempfile = "3"
112 changes: 97 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ A basic config looks like this:
```toml
[global]
default_profile = "personal"
check_updates = true

[profiles.personal]
install_dir = "C:\\Program Files\\Binary Ninja Personal"
Expand Down Expand Up @@ -67,40 +66,105 @@ bn-loader --list
# Launch with debug output
bn-loader personal --debug

# Check for updates
bn-loader --check-update
# Force color output (also: "always", "never", or default "auto")
bn-loader --color always profile diff personal commercial
```

### Commands

**init** - Create a new profile from an existing one:
Profile management commands are grouped under `bn-loader profile`. The flat launch shortcut (`bn-loader <name>`) and `--list` remain at the top level.

**profile init** - Create a new profile from an existing one:
```bash
bn-loader init dev --template personal --config-dir ~/bn-dev-config
bn-loader profile init dev --template personal --config-dir ~/bn-dev-config
```
This copies the license and install directory from the template but gives the new profile its own config directory.

**sync** - Copy settings between profiles:
**profile install** - Install Binary Ninja from an archive and register a profile:

```bash
# Linux: extract a .zip bundle (registers profile 'stable' at ~/.config/bn-loader/profiles/stable)
bn-loader profile install ~/Downloads/binaryninja_linux_X.Y.Z_personal.zip --dest /opt/binaryninja/stable

# Windows: extract an NSIS installer .exe (requires 7-Zip)
bn-loader profile install C:\Downloads\binaryninja_win64_X.Y.Z_personal.exe --dest "C:\Program Files\Binary Ninja Personal"

# Override the auto-derived profile name and/or config dir
bn-loader profile install ~/Downloads/binaryninja_linux_X.Y.Z_personal.zip \
--dest /opt/binaryninja/stable \
--name personal-stable \
--config-dir ~/.binaryninja-personal-stable

# Extract only -- don't touch the config file
bn-loader profile install ~/Downloads/binaryninja_linux_X.Y.Z_personal.zip --dest /opt/binaryninja/stable --no-register
```

By default, `bn-loader profile install` registers a new profile in your config:
- `--name` defaults to a sanitized basename of `--dest` (`/opt/binaryninja/stable` -> `stable`). Invalid characters become underscores.
- `--config-dir` defaults to `~/.config/bn-loader/profiles/<name>`.
- The profile name is the only uniqueness constraint. If you omit `--name` and the derived name is already taken, bn-loader auto-bumps to `<name>-2`, `<name>-3`, etc.
- If you explicitly pass `--name` and that name already exists, bn-loader errors out -- an explicit name is never silently rewritten.
- `--config-dir` can be shared across multiple profiles (e.g., two installations that share plugins/settings). bn-loader does not check or warn if the config dir already contains files.

Pass `--no-register` to skip profile registration entirely (install becomes a pure extract).

For the Windows NSIS path, `bn-loader` shells out to 7z. Resolution order is `--seven-zip` flag, then `[install] seven_zip` in your config, then `$PATH`. NSIS-only artifacts (`$PLUGINSDIR/`, installer images, VC++ redistributables) are filtered out automatically. ZIP archives have their single top-level directory stripped (so `--dest /opt/binaryninja/stable` produces `binaryninja` directly under it, not nested).

If `--dest` is non-empty, `bn-loader profile install` requires `--force` to overlay-extract on top (existing files are preserved unless they share a name with an entry in the archive). The blast-radius warning lists which profiles will be affected by the install (relevant for updates and shared installations). Pass `--yes` to skip the interactive `[y/N]` confirmation; pass `--force --yes` for a fully unattended overlay install.

**profile update** - Re-extract Binary Ninja into an existing profile's install_dir:
```bash
bn-loader profile update stable ~/Downloads/binaryninja_linux_NEW.zip --yes
```
A thin wrapper over `profile install`. Looks up the profile by name, uses its `install_dir` as the dest, and runs the install logic with `--force` (overlay) and `--no-register` (already registered) implicit. Honors `--yes`, `--dry-run`, `--seven-zip`. Useful for unattended updates.

**profile remove** - Deregister a profile (and optionally delete its on-disk dirs):
```bash
# Just deregister (default): leaves install_dir and config_dir on disk
bn-loader profile remove dev

# Also delete the profile's config_dir
bn-loader profile remove dev --purge

# Also delete the install_dir (only with --purge --force)
bn-loader profile remove dev --purge --force
```
Always shows blast radius before acting (warns if `install_dir` or `config_dir` is shared with other profiles). Refuses to `--purge` a `config_dir` shared with other profiles, and refuses to `--purge --force` an `install_dir` shared with other profiles — remove those profiles first or skip `--purge`/`--force`. Note: editing the config file is a TOML round-trip, so comments and exact formatting are not preserved across removes.

**profile sync** - Copy settings between profiles:
```bash
# Sync from personal to all other profiles
bn-loader sync --from personal
bn-loader profile sync --from personal

# Sync to a specific profile
bn-loader sync --from personal --to commercial
bn-loader profile sync --from personal --to commercial

# Preview changes without applying
bn-loader sync --from personal --dry-run
bn-loader profile sync --from personal --dry-run
```
License files and other sensitive data are excluded by default. You can add more exclusions in the `[sync]` section of your config.

**plugins** - List installed plugins for a profile:
**profile plugins** - List installed plugins for a profile:
```bash
bn-loader profile plugins personal
```

**profile diff** - Compare two profiles:
```bash
bn-loader profile diff personal commercial
```

**profile list** - List available profiles (canonical form: `bn-loader profile list`):
```bash
bn-loader plugins personal
bn-loader profile list
```
This produces the same output as the `--list` shortcut flag (see Usage above).

**diff** - Compare two profiles:
**doctor** - Validate the whole config (read-only):
```bash
bn-loader diff personal commercial
bn-loader doctor
```
Checks each profile's `install_dir` / `executable` / `config_dir`, plus global checks (`[install] seven_zip` if set, orphan dirs under `~/.config/bn-loader/profiles/`). Prints `[OK]` / `[WARN]` / `[FAIL]` per check (to stderr) and a summary line on stdout. Exits 0 if no failures, 1 otherwise — useful for `bn-loader doctor && bn-loader <profile>` patterns and CI.

**completions** - Set up shell completions:
```bash
Expand All @@ -110,6 +174,25 @@ bn-loader completions fish
bn-loader completions powershell
```

### Flag conventions

Standardized across every mutating command:

- `--yes` (`-y`) — skip all interactive `[y/N]` confirmation prompts (just say yes).
- `--force` (`-f`) — override safety checks. Allow destructive defaults that are blocked otherwise (e.g., overlay-extract on a non-empty `--dest`, remove a profile whose `install_dir` is shared, sync without backups).
- `--dry-run` — print what would happen without doing it.

Fully-unattended scripts typically want `--yes --force`. The two are independent — `--yes` answers prompts, `--force` overrides safety blocks.

### Output streams

`bn-loader` follows standard stdout/stderr discipline so subcommand output composes cleanly with shell pipelines:

- **stdout:** subcommand result data (profile lists, diff entries, plugin tables, doctor summary).
- **stderr:** status updates, warnings, errors, prompts.

So `bn-loader profile plugins commercial | grep ...` filters only the actual plugin lines, and `bn-loader profile list 2>/dev/null` outputs only the profile list with no header noise.

## Shell Completions

bn-loader supports tab completion for profile names and commands. Run `bn-loader completions <shell>` for setup instructions specific to your shell.
Expand All @@ -122,7 +205,6 @@ These go in the `[global]` section:
|--------|---------|-------------|
| `default_profile` | none | Profile to launch when no argument given |
| `color` | `"auto"` | Color output: `"auto"`, `"always"`, `"never"` |
| `check_updates` | `true` | Check GitHub for new releases on launch |
| `backup_retention` | `5` | Number of sync backups to keep (0 = unlimited) |
| `debug` | `false` | Enable debug logging globally |

Expand Down Expand Up @@ -168,7 +250,7 @@ exclusions = ["my-custom-dir/", "*.tmp"]
Or use the `--exclude` flag for one-off exclusions:

```bash
bn-loader sync --from personal --exclude "temp/"
bn-loader profile sync --from personal --exclude "temp/"
```

## License
Expand Down
15 changes: 14 additions & 1 deletion example.config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
[global]
# default_profile = "personal" # Launch this profile when no argument given
# color = "auto" # Color output: "auto", "always", "never"
# check_updates = true # Check for updates on launch
# backup_retention = 5 # Keep this many sync backups (0 = unlimited)
# debug = false # Enable debug logging globally

Expand All @@ -26,6 +25,20 @@
# [sync]
# exclusions = ["my-custom-dir/", "*.tmp"]

# ============================================================================
# Install Settings (optional)
# ============================================================================
#
# Used by `bn-loader install <archive>`. Currently only seven_zip is configurable.
#
# seven_zip: optional path to 7-Zip's executable, used to extract Windows NSIS
# installer .exe files. Resolution order: --seven-zip CLI flag, then this
# config field, then $PATH lookup. If you have 7-Zip in a non-standard location,
# set it here so you don't have to pass --seven-zip every time.
#
# [install]
# seven_zip = "C:\\Program Files\\7-Zip\\7z.exe"

# ============================================================================
# Profile Examples
# ============================================================================
Expand Down
52 changes: 52 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use crate::config::{Config, Profile};
use anyhow::{Context, Result};
use std::io::{self, Write};
use std::process;

/// Print error to stderr and exit non-zero on Err; exit 0 on Ok. Never returns.
///
/// When `debug` is true, prints the full anyhow cause chain on indented `caused by:`
/// lines after the top message; otherwise prints just the top message.
pub(crate) fn report_and_exit(result: Result<()>, debug: bool) -> ! {
match result {
Ok(()) => process::exit(0),
Err(e) => {
eprintln!("Error: {e}");
if debug {
for cause in e.chain().skip(1) {
eprintln!(" caused by: {cause}");
}
}
process::exit(1);
}
}
}

/// Look up a profile by name. Returns Err with a helpful message including a hint
/// to use `--list` if the profile is missing.
pub(crate) fn resolve_profile<'a>(config: &'a Config, name: &str) -> Result<&'a Profile> {
config.profiles.get(name).ok_or_else(|| {
anyhow::anyhow!("Profile '{name}' not found.\nUse --list to see available profiles.")
})
}

/// Prompt the user with a yes/no question. Default answer is "no" when `default_no`
/// is true (shown as `[y/N]`), otherwise "yes" (shown as `[Y/n]`). Returns Ok(true)
/// when the user answers yes.
pub(crate) fn confirm_prompt(message: &str, default_no: bool) -> Result<bool> {
let suffix = if default_no { "[y/N]" } else { "[Y/n]" };
print!("{message} {suffix} ");
io::stdout().flush().context("Failed to flush stdout")?;

let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;

let trimmed = input.trim();
Ok(if default_no {
trimmed.eq_ignore_ascii_case("y") || trimmed.eq_ignore_ascii_case("yes")
} else {
!(trimmed.eq_ignore_ascii_case("n") || trimmed.eq_ignore_ascii_case("no"))
})
}
28 changes: 0 additions & 28 deletions src/colors.rs

This file was deleted.

Loading
Loading