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
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ jobs:
- name: Verify installer script
run: bash -n install.sh

- name: Verify uninstaller script
run: bash -n uninstall.sh

- name: Test installer checksum verification
run: bash scripts/test-installer-checksum.sh

Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ Setup and management:
mobilecli credentials revoke <id> Revoke one paired mobile credential
mobilecli status Show daemon status, active sessions, connections
mobilecli stop Stop the daemon
mobilecli uninstall Remove MobileCLI completely (daemon, autostart, hook, config, binary)

Daemon lifecycle:
mobilecli daemon [--port PORT] Start daemon manually (default port: 9847)
Expand Down Expand Up @@ -275,6 +276,22 @@ This adds a one-liner to your `.bashrc`, `.zshrc`, `config.fish`, or PowerShell
MOBILECLI_NO_AUTO_LAUNCH=1 bash
```

### Uninstall

To remove MobileCLI completely, run the built-in uninstaller:

```bash
mobilecli uninstall
```

This stops the daemon, removes daemon autostart (systemd / launchd / Task Scheduler), strips the shell auto-launch hook from your shell config, deletes the config directory (`~/.mobilecli`, including paired credentials), and removes the binary. Use `--keep-config` to preserve `~/.mobilecli`, `--keep-binary` to leave the binary in place, and `-y`/`--yes` to skip the confirmation prompt.

If the binary is missing or broken, you can run the standalone uninstall script instead, which delegates to `mobilecli uninstall` when available and otherwise performs the same cleanup manually:

```bash
curl -fsSL https://raw.githubusercontent.com/MobileCLI/mobilecli/main/uninstall.sh | bash
```

<br/>

## Platform support
Expand Down Expand Up @@ -384,13 +401,15 @@ MobileCLI/
│ ├── setup.rs # Interactive setup wizard + QR code
│ ├── shell_hook.rs # Shell auto-launch integration
│ ├── autostart.rs # OS-level daemon autostart
│ ├── uninstall.rs # Native uninstaller (reverses install + setup)
│ ├── link.rs # Session attachment (tmux-style)
│ ├── session.rs # Session metadata structures
│ ├── platform.rs # Cross-platform utilities
│ └── qr.rs # QR code rendering
├── mobile/ # React Native app (separate git repo)
├── website/ # Marketing site (Astro + Tailwind)
├── install.sh # One-line installer script
├── uninstall.sh # One-line uninstaller script
├── .github/workflows/ # CI, release packaging, Claude Code review
└── docs/ # Architecture docs + screenshots
```
Expand Down
11 changes: 11 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod session;
mod setup;
mod shell_hook;
mod tmux;
mod uninstall;

use clap::{Parser, Subcommand};
use colored::Colorize;
Expand Down Expand Up @@ -109,6 +110,9 @@ enum Commands {
#[command(subcommand)]
command: shell_hook::ShellHookCommand,
},

/// Remove MobileCLI: stop the daemon, remove autostart, shell hook, config, and binary
Uninstall(uninstall::UninstallArgs),
}

#[derive(Subcommand)]
Expand Down Expand Up @@ -228,6 +232,13 @@ async fn main() -> ExitCode {
ExitCode::FAILURE
}
},
Commands::Uninstall(uninstall_args) => match uninstall::run(uninstall_args.clone()) {
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{}: {}", "Uninstall error".red().bold(), e);
ExitCode::FAILURE
}
},
};
}

Expand Down
175 changes: 175 additions & 0 deletions cli/src/uninstall.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//! Native uninstaller for MobileCLI.
//!
//! Reverses everything `install.sh` and the setup wizard put in place:
//! - Stops the running daemon
//! - Removes daemon autostart (systemd / launchd / Task Scheduler)
//! - Removes the shell auto-launch hook from rc files
//! - Deletes the config directory (`~/.mobilecli`), including paired credentials
//! - Removes the `mobilecli` binary itself
//!
//! Use `--yes` to skip the confirmation prompt and `--keep-config` to preserve
//! `~/.mobilecli` (paired credentials, sessions, logs).

use crate::{autostart, daemon, platform, shell_hook};
use clap::Args;
use colored::Colorize;
use std::io::Write;
use std::path::PathBuf;

#[derive(Debug, Clone, Default, Args)]
pub struct UninstallArgs {
/// Skip the confirmation prompt
#[arg(long, short = 'y')]
pub yes: bool,

/// Keep the config directory (~/.mobilecli) and paired credentials
#[arg(long = "keep-config")]
pub keep_config: bool,

/// Don't remove the mobilecli binary itself
#[arg(long = "keep-binary")]
pub keep_binary: bool,
}

pub fn run(args: UninstallArgs) -> Result<(), Box<dyn std::error::Error>> {
let config_dir = platform::config_dir();
let binary = std::env::current_exe().ok();

println!("{}", "This will remove MobileCLI from your system:".bold());
println!(" • Stop the background daemon");
println!(" • Remove daemon autostart (login service)");
println!(" • Remove the shell auto-launch hook from your shell config");
if !args.keep_config {
println!(
" • Delete {} {}",
config_dir.display().to_string().cyan(),
"(paired credentials, sessions, logs)".dimmed()
);
}
if !args.keep_binary {
if let Some(ref exe) = binary {
println!(
" • Remove the binary at {}",
exe.display().to_string().cyan()
);
}
}
println!();

if !args.yes && !confirm("Proceed with uninstall?")? {
println!("{} Uninstall cancelled.", "○".dimmed());
return Ok(());
}

// 1. Stop the daemon so no process keeps the port or config files busy.
if let Some(pid) = daemon::get_pid() {
if platform::terminate_process(pid) {
println!("{} Stopped daemon (PID: {})", "✓".green(), pid);
} else {
eprintln!("{} Could not stop daemon (PID: {})", "!".yellow(), pid);
}
} else {
println!("{} Daemon not running", "·".dimmed());
}

// 2. Remove autostart (best-effort; reuses platform-specific logic).
if let Err(e) = autostart::run(autostart::AutostartCommand::Uninstall) {
eprintln!("{} Autostart cleanup: {}", "!".yellow(), e);
}

// 3. Remove the shell auto-launch hook from rc files.
if let Err(e) = shell_hook::run(shell_hook::ShellHookCommand::Uninstall) {
eprintln!("{} Shell hook cleanup: {}", "!".yellow(), e);
}

// 4. Delete the config directory (credentials, sessions, logs, uploads).
if args.keep_config {
println!(
"{} Kept config directory: {}",
"·".dimmed(),
config_dir.display().to_string().dimmed()
);
} else if config_dir.exists() {
match std::fs::remove_dir_all(&config_dir) {
Ok(_) => println!(
"{} Removed config directory: {}",
"✓".green(),
config_dir.display()
),
Err(e) => eprintln!(
"{} Failed to remove {}: {}",
"✗".red(),
config_dir.display(),
e
),
}
} else {
println!("{} Config directory already gone", "·".dimmed());
}

// 5. Remove the binary itself.
if !args.keep_binary {
if let Some(exe) = binary {
remove_binary(&exe);
}
}

println!();
println!(
"{}",
"MobileCLI has been uninstalled. Thanks for trying it!".green()
);
Ok(())
}

/// Remove the running binary. On Unix the file can be unlinked while the process
/// is still executing (the inode survives until exit). On Windows a running
/// executable cannot be deleted, so we fall back to printing manual instructions.
fn remove_binary(exe: &PathBuf) {
#[cfg(windows)]
{
// Windows locks the running executable; schedule a best-effort delete and
// tell the user how to finish if it fails.
match std::fs::remove_file(exe) {
Ok(_) => println!("{} Removed binary: {}", "✓".green(), exe.display()),
Err(_) => {
println!(
"{} Could not remove the binary while it is running.",
"!".yellow()
);
println!(
" Delete it manually after this process exits: {}",
exe.display().to_string().cyan()
);
}
}
}

#[cfg(not(windows))]
{
match std::fs::remove_file(exe) {
Ok(_) => println!("{} Removed binary: {}", "✓".green(), exe.display()),
Err(e) => {
eprintln!("{} Failed to remove {}: {}", "✗".red(), exe.display(), e);
println!(
" Remove it manually: {}",
format!("rm {}", exe.display()).cyan()
);
}
}
}
}

/// Prompt the user for a yes/no confirmation. Defaults to "no" on EOF or empty input.
fn confirm(question: &str) -> std::io::Result<bool> {
print!("{} [y/N] ", question);
std::io::stdout().flush()?;

let mut answer = String::new();
if std::io::stdin().read_line(&mut answer)? == 0 {
return Ok(false);
}

let answer = answer.trim().to_lowercase();
Ok(answer == "y" || answer == "yes")
}
110 changes: 110 additions & 0 deletions uninstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/bin/bash
# MobileCLI Uninstaller (Linux & macOS)
# Usage: curl -fsSL https://raw.githubusercontent.com/MobileCLI/mobilecli/main/uninstall.sh | bash
#
# If the `mobilecli` binary is on your PATH this simply delegates to the built-in
# `mobilecli uninstall`, which is the authoritative cleanup path. If the binary is
# missing or broken, this script falls back to removing everything the installer
# and setup wizard create.

set -euo pipefail

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color

BINARY_NAME="mobilecli"
CONFIG_DIR="${HOME}/.mobilecli"

info() { echo -e "${CYAN}$1${NC}"; }
success() { echo -e "${GREEN}✓ $1${NC}"; }
warn() { echo -e "${YELLOW}⚠ $1${NC}"; }

info "╔══════════════════════════════════════════════════════════════╗"
info "║ 📱 MobileCLI Uninstaller ║"
info "╚══════════════════════════════════════════════════════════════╝"
echo

# Preferred path: let the binary uninstall itself (handles autostart, shell hook,
# config, and the binary in one consistent place).
if command -v "$BINARY_NAME" >/dev/null 2>&1; then
info "Found ${BINARY_NAME} on PATH — running its built-in uninstaller..."
if "$BINARY_NAME" uninstall --yes; then
exit 0
fi
warn "Built-in uninstaller did not finish cleanly; running manual cleanup..."
fi

# Fallback: manual cleanup when the binary is missing or failed.
warn "Performing manual cleanup."

# Stop any running daemon by removing its socket/pid is handled by the binary; without
# it we just leave the process to exit. Best-effort kill via the recorded PID file.
if [ -f "${CONFIG_DIR}/daemon.pid" ]; then
pid="$(cat "${CONFIG_DIR}/daemon.pid" 2>/dev/null || true)"
if [ -n "${pid}" ] && kill -0 "${pid}" 2>/dev/null; then
kill "${pid}" 2>/dev/null && success "Stopped daemon (PID: ${pid})"
fi
fi

# Remove daemon autostart (Linux systemd user unit).
if command -v systemctl >/dev/null 2>&1; then
systemctl --user disable --now mobilecli.service >/dev/null 2>&1 || true
fi
SYSTEMD_UNIT="${HOME}/.config/systemd/user/mobilecli.service"
if [ -f "${SYSTEMD_UNIT}" ]; then
rm -f "${SYSTEMD_UNIT}"
success "Removed systemd unit: ${SYSTEMD_UNIT}"
systemctl --user daemon-reload >/dev/null 2>&1 || true
fi

# Remove daemon autostart (macOS launchd agent).
LAUNCHD_PLIST="${HOME}/Library/LaunchAgents/com.mobilecli.daemon.plist"
if [ -f "${LAUNCHD_PLIST}" ]; then
launchctl unload -w "${LAUNCHD_PLIST}" >/dev/null 2>&1 || true
rm -f "${LAUNCHD_PLIST}"
success "Removed launchd agent: ${LAUNCHD_PLIST}"
fi

# Remove the shell auto-launch hook from rc files (sentinel-delimited block).
BEGIN_MARKER="# >>> mobilecli auto-launch >>>"
END_MARKER="# <<< mobilecli auto-launch <<<"
for rc in "${HOME}/.bashrc" "${HOME}/.bash_profile" "${HOME}/.profile" "${HOME}/.zshrc" "${HOME}/.config/fish/config.fish"; do
if [ -f "${rc}" ] && grep -qF "${BEGIN_MARKER}" "${rc}"; then
tmp="$(mktemp)"
# Delete everything between (and including) the sentinel markers.
awk -v b="${BEGIN_MARKER}" -v e="${END_MARKER}" '
$0 == b {skip=1; next}
skip && $0 == e {skip=0; next}
!skip {print}
' "${rc}" > "${tmp}"
mv "${tmp}" "${rc}"
success "Removed shell hook from ${rc}"
fi
done

# Remove the config directory (paired credentials, sessions, logs).
if [ -d "${CONFIG_DIR}" ]; then
rm -rf "${CONFIG_DIR}"
success "Removed config directory: ${CONFIG_DIR}"
fi

# Remove the binary from common install locations.
for dir in "${HOME}/.local/bin" "${HOME}/bin" "/usr/local/bin"; do
target="${dir}/${BINARY_NAME}"
if [ -f "${target}" ]; then
if rm -f "${target}" 2>/dev/null; then
success "Removed binary: ${target}"
elif command -v sudo >/dev/null 2>&1; then
sudo rm -f "${target}" && success "Removed binary: ${target}"
else
warn "Could not remove ${target} (insufficient permissions)"
fi
fi
done

echo
success "MobileCLI has been uninstalled."
Loading