Files
composiohq__composio/install.sh
T
jkomyno 9f10adcc8d fix(installer): escape the install dir before writing it into shell rc files
install.sh's inline PATH fallback interpolated COMPOSIO_INSTALL_DIR raw
into the fish and zsh rc lines, and escaped only " in the bash arm. Since
that fallback is exactly what runs when `composio install` rejects a path
and exits non-zero, a directory containing $(...) or a backtick was
written unescaped into ~/.zshrc or config.fish and became live command
substitution at the next shell start.

Escape backslash, backtick, $ and " for every arm before the value is
embedded, skipping the backtick for fish (which neither substitutes it
nor recognizes \` inside double quotes). The $HOME prefix is re-applied
after escaping so the intended variable reference survives while a
user-supplied $ does not.
2026-08-04 19:15:59 +05:30

470 lines
17 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
COMPOSIO_GITHUB_OWNER=${COMPOSIO_GITHUB_OWNER-"ComposioHQ"}
COMPOSIO_GITHUB_REPO=${COMPOSIO_GITHUB_REPO-"composio"}
COMPOSIO_GITHUB_URL=${COMPOSIO_GITHUB_URL-"https://github.com"}
COMPOSIO_GITHUB_API_BASE_URL=${COMPOSIO_GITHUB_API_BASE_URL:-}
COMPOSIO_INSTALL_DIR=${COMPOSIO_INSTALL_DIR:-$HOME/.composio}
# --- Input validation ---
# Only allow HTTPS URLs for the download source.
if [[ ! "$COMPOSIO_GITHUB_URL" =~ ^https:// ]]; then
echo "error: COMPOSIO_GITHUB_URL must start with https:// (got \"$COMPOSIO_GITHUB_URL\")" >&2
exit 1
fi
# Owner and repo must be safe identifiers (alphanumeric, hyphens, underscores, dots).
if [[ ! "$COMPOSIO_GITHUB_OWNER" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "error: COMPOSIO_GITHUB_OWNER contains invalid characters (got \"$COMPOSIO_GITHUB_OWNER\")" >&2
exit 1
fi
if [[ ! "$COMPOSIO_GITHUB_REPO" =~ ^[a-zA-Z0-9._-]+$ ]]; then
echo "error: COMPOSIO_GITHUB_REPO contains invalid characters (got \"$COMPOSIO_GITHUB_REPO\")" >&2
exit 1
fi
if [[ -n "$COMPOSIO_GITHUB_API_BASE_URL" && ! "$COMPOSIO_GITHUB_API_BASE_URL" =~ ^https:// ]]; then
echo "error: COMPOSIO_GITHUB_API_BASE_URL must start with https:// (got \"$COMPOSIO_GITHUB_API_BASE_URL\")" >&2
exit 1
fi
github_repo="$COMPOSIO_GITHUB_URL/$COMPOSIO_GITHUB_OWNER/$COMPOSIO_GITHUB_REPO"
if [[ -n "$COMPOSIO_GITHUB_API_BASE_URL" ]]; then
github_api_base="${COMPOSIO_GITHUB_API_BASE_URL%/}"
elif [[ "$COMPOSIO_GITHUB_URL" = "https://github.com" ]]; then
github_api_base="https://api.github.com"
else
github_api_base="${COMPOSIO_GITHUB_URL%/}/api/v3"
fi
github_api_repo="$github_api_base/repos/$COMPOSIO_GITHUB_OWNER/$COMPOSIO_GITHUB_REPO"
# --- Colors (only when interactive) ---
Color_Off='' Red='' Green='' Dim='' Bold_White='' Bold_Green=''
if [[ -t 1 ]]; then
Color_Off='\033[0m'
Red='\033[0;31m'
Green='\033[0;32m'
Dim='\033[0;2m'
Bold_Green='\033[1;32m'
Bold_White='\033[1m'
fi
error() { echo -e "${Red}error${Color_Off}:" "$@" >&2; exit 1; }
warn() { echo -e "${Red}warning${Color_Off}:" "$@" >&2; }
info() { echo -e "${Dim}$*${Color_Off}"; }
info_bold() { echo -e "${Bold_White}$*${Color_Off}"; }
success() { echo -e "${Green}$*${Color_Off}"; }
tildify() {
if [[ $1 = $HOME/* ]]; then
echo "~/${1#$HOME/}"
else
echo "$1"
fi
}
# Escape a value for embedding inside a double-quoted shell rc line, so an
# install directory containing `$(...)`, a backtick, or a quote is written as
# literal text instead of becoming live code the next time the rc is sourced.
# Backslashes must be escaped first. The second argument selects whether
# backticks are escaped: bash and zsh perform backtick substitution inside
# double quotes, fish does not — and fish does not recognize \` there either,
# so escaping it for fish would corrupt the path.
escape_for_double_quotes() {
local value=$1 escape_backticks=${2:-true}
value=${value//\\/\\\\}
if [[ $escape_backticks = true ]]; then
value=${value//\`/\\\`}
fi
value=${value//\$/\\\$}
value=${value//\"/\\\"}
printf '%s' "$value"
}
install_agent=false
install_plugins=true
version_arg=""
case "${COMPOSIO_INSTALL_PLUGINS:-1}" in
1) ;;
0) install_plugins=false ;;
*) error 'COMPOSIO_INSTALL_PLUGINS must be 1 or 0' ;;
esac
while [[ $# -gt 0 ]]; do
case "$1" in
--agent)
install_agent=true
shift
;;
--no-plugins)
install_plugins=false
shift
;;
-h|--help)
echo 'Usage: install.sh [--agent] [--no-plugins] [version-tag] (e.g. "@composio/cli@0.1.32")'
echo ' --agent After installing, sign up/log in as a Composio agent.'
echo ' --no-plugins Skip installing plugins for detected agent hosts.'
echo ' Set COMPOSIO_INSTALL_PLUGINS=0 to skip plugin installation in automation.'
exit 0
;;
--*)
error "Unknown option: $1"
;;
*)
if [[ -n "$version_arg" ]]; then
error 'Too many arguments. Usage: install.sh [--agent] [--no-plugins] [version-tag] (e.g. "@composio/cli@0.1.32")'
fi
version_arg=$1
shift
;;
esac
done
# --- Platform detection ---
platform=$(uname -ms)
case $platform in
'MINGW64'* | 'MSYS'* | 'CYGWIN'*)
error 'Windows is not supported. Please use WSL (https://learn.microsoft.com/windows/wsl/install) and run this script inside your WSL distribution.'
;;
esac
# --- Prerequisites ---
command -v curl >/dev/null || error 'curl is required to install Composio CLI'
command -v unzip >/dev/null || error 'unzip is required to install Composio CLI'
case $platform in
'Darwin x86_64') target=darwin-x64 ;;
'Darwin arm64') target=darwin-aarch64 ;;
'Linux aarch64' | 'Linux arm64')
target=linux-aarch64 ;;
'Linux x86_64') target=linux-x64 ;;
*) error "Unsupported platform: $platform" ;;
esac
# Rosetta 2 detection on macOS
if [[ $target = darwin-x64 ]]; then
if [[ $(sysctl -n sysctl.proc_translated 2>/dev/null) = 1 ]]; then
target=darwin-aarch64
info "Your shell is running in Rosetta 2. Downloading for $target instead"
fi
fi
archive_name="composio-$target.zip"
resolve_latest_cli_release() {
local page release_json release_line
for page in 1 2 3 4 5; do
release_json=$(curl --fail --silent --location "$github_api_repo/releases?per_page=100&page=$page") || return 1
release_line=$(printf '%s\n' "$release_json" \
| sed 's/"tag_name"/\
"tag_name"/g; s/"browser_download_url"/\
"browser_download_url"/g' \
| awk -v asset_name="$archive_name" '
BEGIN {
tag = ""
stable_cli_release = "^@composio/cli@[0-9]+\\.[0-9]+\\.[0-9]+$"
}
/"tag_name":[[:space:]]*"/ {
tag = $0
sub(/^.*"tag_name":[[:space:]]*"/, "", tag)
sub(/".*$/, "", tag)
if (tag !~ stable_cli_release) {
tag = ""
}
}
tag != "" && /"browser_download_url":[[:space:]]*"/ && index($0, "/" asset_name "\"") > 0 {
url = $0
sub(/^.*"browser_download_url":[[:space:]]*"/, "", url)
sub(/".*$/, "", url)
print tag "\t" url
exit
}
')
if [[ -n "$release_line" ]]; then
printf '%s\n' "$release_line"
return 0
fi
if ! printf '%s\n' "$release_json" | grep -q '"tag_name"'; then
break
fi
done
return 1
}
# --- Version resolution ---
if [[ -z "$version_arg" ]]; then
info "Finding latest CLI release..."
latest_release=$(resolve_latest_cli_release) ||
error "Failed to determine the latest CLI release with a $archive_name asset. Please specify a version manually."
version=${latest_release%%$'\t'*}
archive_url=${latest_release#*$'\t'}
info "Found latest version: $version"
else
version=$version_arg
archive_url="$github_repo/releases/download/$version/$archive_name"
fi
# --- Download into temp directory ---
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
checksums_url="$github_repo/releases/download/$version/checksums.txt"
info "Installing Composio CLI $version for $target"
info "Downloading..."
curl --fail --location --progress-bar --output "$tmpdir/$archive_name" "$archive_url" ||
error "Failed to download from \"$archive_url\""
# --- Checksum verification ---
if curl --fail --silent --location --output "$tmpdir/checksums.txt" "$checksums_url" 2>/dev/null; then
expected=$(grep "$archive_name" "$tmpdir/checksums.txt" | awk '{print $1}')
if [[ -n "$expected" ]]; then
if command -v sha256sum &>/dev/null; then
actual=$(sha256sum "$tmpdir/$archive_name" | awk '{print $1}')
elif command -v shasum &>/dev/null; then
actual=$(shasum -a 256 "$tmpdir/$archive_name" | awk '{print $1}')
else
actual=""
warn "No SHA-256 utility found — skipping verification"
fi
if [[ -n "$actual" && "$expected" != "$actual" ]]; then
error "Checksum mismatch for $archive_name\n Expected: $expected\n Actual: $actual"
fi
if [[ -n "$actual" ]]; then
info "Checksum verified"
fi
else
warn "No checksum entry found for $archive_name — skipping verification"
fi
else
info "No checksums.txt in release — skipping verification"
fi
# --- Extract and install ---
info "Extracting..."
unzip -oqd "$tmpdir" "$tmpdir/$archive_name" ||
error 'Failed to extract archive'
mkdir -p "$COMPOSIO_INSTALL_DIR" ||
error "Failed to create install directory \"$COMPOSIO_INSTALL_DIR\""
exe="$COMPOSIO_INSTALL_DIR/composio"
release_tag_file="$COMPOSIO_INSTALL_DIR/release-tag.txt"
install_bundle_support_files() {
local source_dir="$1"
local installed_count=0
while IFS= read -r -d '' source_path; do
local relative_path=${source_path#"$source_dir"/}
local target_path="$COMPOSIO_INSTALL_DIR/$relative_path"
mkdir -p "$(dirname "$target_path")" ||
error "Failed to create support file directory \"$(dirname "$target_path")\""
mv "$source_path" "$target_path" ||
error "Failed to install support file \"$relative_path\""
installed_count=$((installed_count + 1))
done < <(find "$source_dir" -mindepth 1 -type f ! -path "$source_dir/composio" -print0)
if (( installed_count == 0 )); then
warn "This release archive does not include any bundled support files beyond the main binary. Some CLI features may be unavailable in this version."
fi
}
# Handle nested directory structure (composio-<target>/composio)
if [[ -f "$tmpdir/composio-$target/composio" ]]; then
mv "$tmpdir/composio-$target/composio" "$exe"
install_bundle_support_files "$tmpdir/composio-$target"
elif [[ -f "$tmpdir/composio" ]]; then
mv "$tmpdir/composio" "$exe"
install_bundle_support_files "$tmpdir"
else
error 'Binary not found in extracted archive'
fi
chmod +x "$exe" ||
error 'Failed to set permissions on executable'
printf '%s\n' "$version" > "$release_tag_file" ||
error "Failed to write install metadata to \"$release_tag_file\""
success "Composio CLI was installed successfully to $Bold_Green$(tildify "$exe")"
# --- Shell integration (PATH + completions) ---
# Delegate to the CLI's own install command, which handles:
# - Idempotent PATH setup in the correct rc file
# - Shell completions installation
# If the binary can't run (e.g. missing runtime), fall back to inline setup.
#
# Run it with stderr left on the terminal. `composio install` reports which rc
# file it changed and how to reload the shell, and it treats a redirected stderr
# as "nobody is watching" — capturing that stream, even to replay it later, is
# what makes the PATH guidance disappear. The version probe keeps a binary that
# cannot run at all from spilling loader errors before the fallback takes over.
echo
if "$exe" --version >/dev/null 2>&1 &&
COMPOSIO_INSTALL_DIR="$COMPOSIO_INSTALL_DIR" COMPOSIO_CLI_INVOCATION_ORIGIN=installer "$exe" install; then
: # the CLI printed its own shell-integration guidance
else
info "Setting up shell integration..."
refresh_command=''
# Every rc line below embeds one of these inside double quotes. `$HOME` is
# re-applied after escaping so the intended variable reference survives as
# a literal `$HOME` while any user-supplied `$` stays escaped.
escaped_install_dir=$(escape_for_double_quotes "$COMPOSIO_INSTALL_DIR")
escaped_install_dir_fish=$(escape_for_double_quotes "$COMPOSIO_INSTALL_DIR" false)
home_relative_install_dir=$escaped_install_dir
if [[ $COMPOSIO_INSTALL_DIR = "$HOME"/* ]]; then
home_relative_install_dir='$HOME/'$(escape_for_double_quotes "${COMPOSIO_INSTALL_DIR#"$HOME"/}")
fi
shell_name=$(basename "${SHELL:-}")
marker='# Composio CLI'
case $shell_name in
fish)
commands=(
"set --export COMPOSIO_INSTALL_DIR \"$escaped_install_dir_fish\""
"set --export PATH \$COMPOSIO_INSTALL_DIR \$PATH"
)
fish_config=$HOME/.config/fish/config.fish
if [[ -w $fish_config ]] || [[ -w $(dirname "$fish_config") ]]; then
mkdir -p "$(dirname "$fish_config")"
if ! grep -qxF "$marker" "$fish_config" 2>/dev/null; then
{ echo -e "\n$marker"; for cmd in "${commands[@]}"; do echo "$cmd"; done; } >>"$fish_config"
info "Added \"$(tildify "$COMPOSIO_INSTALL_DIR")\" to \$PATH in \"$(tildify "$fish_config")\""
else
info "PATH already configured in \"$(tildify "$fish_config")\""
fi
refresh_command="source $(tildify "$fish_config")"
else
echo "Manually add the directory to $(tildify "$fish_config") (or similar):"
for cmd in "${commands[@]}"; do info_bold " $cmd"; done
fi
;;
zsh)
commands=(
"export COMPOSIO_INSTALL_DIR=\"$escaped_install_dir\""
"export PATH=\"\$COMPOSIO_INSTALL_DIR:\$PATH\""
)
zsh_config=$HOME/.zshrc
if [[ ! -f $zsh_config && -w $(dirname "$zsh_config") ]]; then touch "$zsh_config"; fi
if [[ -w $zsh_config ]]; then
if ! grep -qxF "$marker" "$zsh_config" 2>/dev/null; then
{ echo -e "\n$marker"; for cmd in "${commands[@]}"; do echo "$cmd"; done; } >>"$zsh_config"
info "Added \"$(tildify "$COMPOSIO_INSTALL_DIR")\" to \$PATH in \"$(tildify "$zsh_config")\""
else
info "PATH already configured in \"$(tildify "$zsh_config")\""
fi
refresh_command="source $(tildify "$zsh_config")"
else
echo "Manually add the directory to $(tildify "$zsh_config") (or similar):"
for cmd in "${commands[@]}"; do info_bold " $cmd"; done
fi
;;
bash)
commands=(
"export COMPOSIO_INSTALL_DIR=\"$home_relative_install_dir\""
"export PATH=\"\$COMPOSIO_INSTALL_DIR:\$PATH\""
)
bash_configs=("$HOME/.bashrc" "$HOME/.bash_profile")
if [[ ${XDG_CONFIG_HOME:-} ]]; then
bash_configs+=("$XDG_CONFIG_HOME/.bash_profile" "$XDG_CONFIG_HOME/.bashrc" "$XDG_CONFIG_HOME/bash_profile" "$XDG_CONFIG_HOME/bashrc")
fi
set_manually=true
for bash_config in "${bash_configs[@]}"; do
if [[ -w $bash_config ]]; then
if ! grep -qxF "$marker" "$bash_config" 2>/dev/null; then
{ echo -e "\n$marker"; for cmd in "${commands[@]}"; do echo "$cmd"; done; } >>"$bash_config"
info "Added \"$(tildify "$COMPOSIO_INSTALL_DIR")\" to \$PATH in \"$(tildify "$bash_config")\""
else
info "PATH already configured in \"$(tildify "$bash_config")\""
fi
refresh_command="source $bash_config"
set_manually=false
break
fi
done
if [[ $set_manually = true ]]; then
echo "Manually add the directory to ~/.bashrc (or similar):"
for cmd in "${commands[@]}"; do info_bold " $cmd"; done
fi
;;
*)
echo 'Manually add the directory to ~/.bashrc (or similar):'
info_bold " export COMPOSIO_INSTALL_DIR=\"$home_relative_install_dir\""
info_bold " export PATH=\"\$COMPOSIO_INSTALL_DIR:\$PATH\""
;;
esac
fi
if [[ $install_plugins = true ]]; then
echo
info "Checking for supported agent hosts..."
if ! COMPOSIO_CLI_INVOCATION_ORIGIN=installer "$exe" setup --target auto --yes --if-present; then
error 'Composio CLI was installed, but agent plugin setup failed. Retry with `composio setup --target auto --yes`.'
fi
fi
if [[ $install_agent = true ]]; then
echo
info "Setting up Composio agent login..."
if ! COMPOSIO_CLI_INVOCATION_ORIGIN=installer "$exe" login --agent --no-skill-install; then
error 'Failed to sign up/log in as a Composio agent. If this CLI is already signed in as a regular user, run `composio logout` and then `composio signup` or `composio agent login <composio_agent_key>`.'
fi
fi
echo
# No PATH claim here: whichever branch ran above already reported what it did to
# which file, and whether a reload is needed. Restating it unconditionally would
# promise a configured PATH even on the branches that only printed manual
# instructions.
info "To get started, run:"
echo
if [[ ${refresh_command:-} ]]; then
info_bold " $refresh_command"
fi
info_bold " composio --help"
if [[ $install_agent = true ]]; then
info_bold " composio agent whoami"
else
info_bold " composio login"
fi