add install.sh script and self-update command

This commit is contained in:
Adil Hafeez 2026-03-22 23:05:13 +00:00
parent 1e6f38f772
commit eb30c65796
3 changed files with 281 additions and 0 deletions

View file

@ -3,6 +3,7 @@ pub mod cli_agent;
pub mod down;
pub mod init;
pub mod logs;
pub mod self_update;
pub mod up;
use clap::{Parser, Subcommand};
@ -132,6 +133,14 @@ pub enum Command {
#[arg(long)]
list_templates: bool,
},
/// Update planoai to the latest version
#[command(name = "self-update")]
SelfUpdate {
/// Update to a specific version instead of latest
#[arg(long)]
version: Option<String>,
},
}
#[derive(Subcommand)]
@ -252,6 +261,7 @@ pub async fn run(cli: Cli) -> anyhow::Result<()> {
force,
list_templates,
}) => init::run(template, clean, output, force, list_templates).await,
Some(Command::SelfUpdate { version }) => self_update::run(version.as_deref()).await,
}
}

View file

@ -0,0 +1,139 @@
use std::fs;
use std::os::unix::fs::PermissionsExt;
use anyhow::{bail, Result};
use crate::consts::{PLANO_GITHUB_REPO, PLANO_VERSION};
pub async fn run(target_version: Option<&str>) -> Result<()> {
let green = console::Style::new().green();
let bold = console::Style::new().bold();
let dim = console::Style::new().dim();
let cyan = console::Style::new().cyan();
println!(
"\n{} {}",
bold.apply_to("planoai"),
dim.apply_to("self-update")
);
// Determine target version
let version = if let Some(v) = target_version {
v.to_string()
} else {
println!(" {}", dim.apply_to("Checking for latest version..."));
fetch_latest_version()
.await?
.ok_or_else(|| anyhow::anyhow!("Could not determine latest version"))?
};
let current = PLANO_VERSION;
if version == current && target_version.is_none() {
println!(
"\n {} Already up to date ({})",
green.apply_to(""),
cyan.apply_to(current)
);
return Ok(());
}
println!(
" {} → {}",
dim.apply_to(format!("Current: {current}")),
cyan.apply_to(&version)
);
// Detect platform
let platform = get_platform_slug()?;
// Download URL
let url = format!(
"https://github.com/{PLANO_GITHUB_REPO}/releases/download/{version}/planoai-{platform}.gz"
);
println!(" {}", dim.apply_to(format!("Downloading from {url}...")));
let client = reqwest::Client::new();
let resp = client.get(&url).send().await?;
if !resp.status().is_success() {
bail!(
"Download failed: HTTP {}. Version {} may not exist for platform {}.",
resp.status(),
version,
platform
);
}
let gz_bytes = resp.bytes().await?;
// Decompress
let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]);
let mut binary_data = Vec::new();
std::io::copy(&mut decoder, &mut binary_data)?;
// Find current binary path
let current_exe = std::env::current_exe()?;
let exe_path = current_exe.canonicalize()?;
println!(
" {}",
dim.apply_to(format!("Installing to {}", exe_path.display()))
);
// Write to a temp file next to the binary, then atomically rename
let tmp_path = exe_path.with_extension("update-tmp");
fs::write(&tmp_path, &binary_data)?;
fs::set_permissions(&tmp_path, fs::Permissions::from_mode(0o755))?;
// Atomic replace
fs::rename(&tmp_path, &exe_path)?;
println!(
"\n {} Updated planoai to {}\n",
green.apply_to(""),
bold.apply_to(&version)
);
Ok(())
}
async fn fetch_latest_version() -> Result<Option<String>> {
let url = format!("https://api.github.com/repos/{PLANO_GITHUB_REPO}/releases/latest");
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()?;
let resp = client
.get(&url)
.header("User-Agent", "planoai-cli")
.send()
.await?;
if !resp.status().is_success() {
return Ok(None);
}
let json: serde_json::Value = resp.json().await?;
let tag = json
.get("tag_name")
.and_then(|v| v.as_str())
.map(|s| s.strip_prefix('v').unwrap_or(s).to_string());
Ok(tag)
}
fn get_platform_slug() -> Result<&'static str> {
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
match (os, arch) {
("linux", "x86_64") => Ok("linux-amd64"),
("linux", "aarch64") => Ok("linux-arm64"),
("macos", "aarch64") => Ok("darwin-arm64"),
("macos", "x86_64") => {
bail!("macOS x86_64 (Intel) is not supported.")
}
_ => bail!("Unsupported platform: {os}/{arch}"),
}
}

132
install.sh Executable file
View file

@ -0,0 +1,132 @@
#!/bin/bash
set -euo pipefail
# Plano CLI installer
# Usage: curl -fsSL https://raw.githubusercontent.com/katanemo/plano/main/install.sh | bash
REPO="katanemo/archgw"
BINARY_NAME="planoai"
INSTALL_DIR="${PLANO_INSTALL_DIR:-$HOME/.plano/bin}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
DIM='\033[2m'
BOLD='\033[1m'
RESET='\033[0m'
info() { echo -e "${GREEN}${RESET} $*"; }
error() { echo -e "${RED}${RESET} $*" >&2; }
dim() { echo -e "${DIM}$*${RESET}"; }
# Detect platform
detect_platform() {
local os arch
os="$(uname -s)"
arch="$(uname -m)"
case "$os" in
Linux) os="linux" ;;
Darwin) os="darwin" ;;
*) error "Unsupported OS: $os"; exit 1 ;;
esac
case "$arch" in
x86_64) arch="amd64" ;;
aarch64) arch="arm64" ;;
arm64) arch="arm64" ;;
*) error "Unsupported architecture: $arch"; exit 1 ;;
esac
if [ "$os" = "darwin" ] && [ "$arch" = "amd64" ]; then
error "macOS x86_64 (Intel) is not supported. Pre-built binaries are only available for Apple Silicon (arm64)."
exit 1
fi
echo "${os}-${arch}"
}
# Get latest version from GitHub releases
get_latest_version() {
local version
version=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \
| grep '"tag_name"' \
| sed -E 's/.*"([^"]+)".*/\1/')
echo "$version"
}
main() {
echo -e "\n${BOLD}Plano CLI Installer${RESET}\n"
# Detect platform
local platform
platform="$(detect_platform)"
dim " Platform: $platform"
# Get version
local version="${PLANO_VERSION:-}"
if [ -z "$version" ]; then
dim " Fetching latest version..."
version="$(get_latest_version)"
fi
if [ -z "$version" ]; then
error "Could not determine version. Set PLANO_VERSION or check your internet connection."
exit 1
fi
dim " Version: $version"
# Download URL
local url="https://github.com/${REPO}/releases/download/${version}/planoai-${platform}.gz"
dim " URL: $url"
echo ""
# Create install directory
mkdir -p "$INSTALL_DIR"
# Download and extract
local tmp_gz
tmp_gz="$(mktemp)"
echo -e " ${DIM}Downloading planoai ${version}...${RESET}"
if ! curl -fSL --progress-bar "$url" -o "$tmp_gz"; then
error "Download failed. Check that version $version exists for platform $platform."
rm -f "$tmp_gz"
exit 1
fi
# Decompress
echo -e " ${DIM}Installing to ${INSTALL_DIR}/${BINARY_NAME}...${RESET}"
gzip -d -c "$tmp_gz" > "${INSTALL_DIR}/${BINARY_NAME}"
chmod +x "${INSTALL_DIR}/${BINARY_NAME}"
rm -f "$tmp_gz"
info "Installed planoai ${version} to ${INSTALL_DIR}/${BINARY_NAME}"
# Check if install dir is in PATH
if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then
echo ""
echo -e " ${CYAN}Add to your PATH:${RESET}"
local shell_name
shell_name="$(basename "${SHELL:-/bin/bash}")"
local rc_file
case "$shell_name" in
zsh) rc_file="$HOME/.zshrc" ;;
fish) rc_file="$HOME/.config/fish/config.fish" ;;
*) rc_file="$HOME/.bashrc" ;;
esac
if [ "$shell_name" = "fish" ]; then
echo -e " ${BOLD}set -gx PATH ${INSTALL_DIR} \$PATH${RESET}"
else
echo -e " ${BOLD}export PATH=\"${INSTALL_DIR}:\$PATH\"${RESET}"
fi
echo -e " ${DIM}Add this line to ${rc_file} to make it permanent.${RESET}"
fi
echo ""
info "Run ${BOLD}planoai --help${RESET} to get started."
echo ""
}
main "$@"