From 0563c02dc654e4a21325728079c5b77b637b7077 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Mon, 17 Feb 2025 16:36:22 -0800 Subject: [PATCH 01/22] Install script and Dockerfile for testing on Linux --- install.sh | 381 ++++++++++++++++++++++++++++++++++ install_test/linux/Dockerfile | 17 ++ 2 files changed, 398 insertions(+) create mode 100644 install.sh create mode 100644 install_test/linux/Dockerfile diff --git a/install.sh b/install.sh new file mode 100644 index 00000000..deefdb17 --- /dev/null +++ b/install.sh @@ -0,0 +1,381 @@ +#!/bin/bash +set -e + +APP_NAME="agentstack" +VERSION="0.3.5" +RELEASE_PATH_URL="https://github.com/AgentOps-AI/AgentStack/archive/refs/tags" +CHECKSUM_URL="" +REQUIRED_PYTHON_VERSION=">=3.10,<3.13" +PYTHON_BIN_PATH="" +UV_INSTALLER_URL="https://astral.sh/uv/install.sh" +PRINT_VERBOSE=0 +PRINT_QUIET=0 + +# workflow: +# do we have curl? if not, default to wget (TODO) +# install uv +# do we have a qualified python version? if not, install it with uv +# - do not modify the default `python` in the user's path +# store the path to the python version we installed for use later (ex: /usr/local/bin/python3.10) +# download the agentstack release +# install agentstack system-wide +# add agentstack to the user's path +# add agentstack to the user's shell config +# cleanup + + +say() { + echo "$1" +} + +say_verbose() { + if [ "1" = "$PRINT_VERBOSE" ]; then + echo "[DEBUG] $1" + fi +} + +err() { + if [ "0" = "$PRINT_QUIET" ]; then + local red + local reset + red=$(tput setaf 1 2>/dev/null || echo '') + reset=$(tput sgr0 2>/dev/null || echo '') + say "${red}ERROR${reset}: $1" >&2 + fi + exit 1 +} + +# Check if a command exists and print an error message if it doesn't +need_cmd() { + if ! check_cmd "$1" + then err "need '$1' (command not found)" + fi +} + +# Check if a command exists +check_cmd() { + command -v "$1" > /dev/null 2>&1 + return $? +} + +# Run a command that should never fail. If the command fails execution +# will immediately terminate with an error showing the failing command. +ensure() { + if ! "$@"; then err "command failed: $*"; fi +} + +# Print usage information +usage() { + cat <&1) + local _retval=$? + say_verbose "$_output" + if [ $_retval -ne 0 ]; then + err "uv installation failed: $_output" + fi + + # ensure uv is in PATH + if ! check_cmd uv; then + say_verbose "Adding ~/.local/bin to PATH" + export PATH="$HOME/.local/bin:$PATH" + fi + + # verify uv installation + local _uv_version + _uv_version="$(uv --version 2>/dev/null)" || { + _uv_version=0 + } + if [ -z "$_uv_version" ]; then + err "uv installation failed. Please ensure ~/.local/bin is in your PATH" + else + say "$_uv_version installed successfully!" + fi +} + +# Install the required Python version +setup_python() { + PYTHON_BIN_PATH="$(uv python find "$REQUIRED_PYTHON_VERSION" 2>/dev/null)" || { + PYTHON_BIN_PATH="" + } + if [ -x "$PYTHON_BIN_PATH" ]; then + say "Python $REQUIRED_PYTHON_VERSION is already installed." + return 0 + else + say "Installing Python $REQUIRED_PYTHON_VERSION..." + uv python install "$REQUIRED_PYTHON_VERSION" --preview 2>/dev/null || { + err "Failed to install Python" + } + PYTHON_BIN_PATH="$(uv python find "$REQUIRED_PYTHON_VERSION")" || { + err "Failed to find Python" + } + fi + + if [ -x "$PYTHON_BIN_PATH" ]; then + local _python_version="$($PYTHON_BIN_PATH --version 2>&1)" + say "Python $_python_version installed successfully!" + else + err "Failed to install Python $REQUIRED_PYTHON_VERSION" + fi +} + +# Install the application +install_app() { + say "Installing $APP_NAME..." + + local _zip_ext + _zip_ext=".tar.gz" # TODO detect this + + local _url="$RELEASE_PATH_URL/$VERSION$_zip_ext" + local _dir + _dir="$(ensure mktemp -d)" || return 1 + local _file="$_dir/input$_zip_ext" + local _checksum_file="$_dir/checksum" + + say_verbose "downloading $APP_NAME $VERSION" 1>&2 + say_verbose " from $_url" 1>&2 + say_verbose " to $_file" 1>&2 + + ensure mkdir -p "$_dir" + + # download tar or zip + if ! downloader "$_url" "$_file"; then + say_verbose "failed to download $_url" + sy "Failed to download $APP_NAME $VERSION" + say "(this may be a standard network error, but it may also indicate" + say "that $APP_NAME's release process is not working. When in doubt" + say "please feel free to open an issue!)" + exit 1 + fi + + # download checksum + local _checksum_available=1 + if ! downloader "$CHECKSUM_URL" "$_checksum_file"; then + say_verbose "failed to download checksum file: $CHECKSUM_URL" + say "Skipping checksum verification" + _checksum_available=0 + fi + + # verify checksum + # github action generates checksums in the following format: + # 0.3.4.tar.gz ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb + # 0.3.4.zip 0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f + if [ "1" = "$_checksum_available" ]; then + # TODO this needs to be tested. + say_verbose "verifying checksum" + local _all_checksums="$(cat "$_checksum_file")" + local _checksum_value="$(echo "$_all_checksums" | grep "$VERSION$_zip_ext" | awk '{print $1}')" + verify_sha256_checksum "$_file" "$_checksum_value" + fi + + # unpack the archive + case "$_zip_ext" in + ".zip") + ensure unzip -q "$_file" -d "$_dir" + ;; + + ".tar."*) + ensure tar xf "$_file" --strip-components 1 -C "$_dir" + ;; + *) + err "unknown archive format: $_zip_ext" + ;; + esac + + # run setup + # TODO we should install to $HOME/.local/bin + # TODO if I install this system-wide with a specific python version, will + # setup py correctly configure it to use that python version after install? + local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { + err "Failed to find user site packages directory" + } + say_verbose "Installing to $_packages_dir" + # TODO silence output + local _install_out + _install_out="$(uv pip install --python=$PYTHON_BIN_PATH --target=$_packages_dir --directory=$_dir . 2>&1)" || { + err "Failed to install $APP_NAME" + } + say_verbose "$_install_out" + make_bin "$PYTHON_BIN_PATH" "$HOME/.local/bin/$APP_NAME" + + # verify installation + ensure "$APP_NAME" --version + + # cleanup + rm -rf "$_dir" + + say "$APP_NAME installed successfully!" + # TODO retval +} + + +make_bin() { + local _bin_content=$(cat < $2 + chmod +x $2 +} + +# This wraps curl or wget. Try curl first, if not installed, use wget instead. +downloader() { + local _cmd + if check_cmd curl; then + _cmd=curl + elif check_cmd wget; then + _cmd=wget + else + err "need curl or wget (command not found)" + return 1 + fi + + local _out + if [ "$_cmd" = curl ]; then + _out="$(curl -sSfL "$1" -o "$2" 2>&1)" || { + say_verbose "$_out" + return 1 + } + elif [ "$_cmd" = wget ]; then + _out="$(wget -q "$1" -O "$2" 2>&1)" || { + say_verbose "$_out" + return 1 + } + else + err "Unknown downloader" + return 1 + fi +} + +verify_sha256_checksum() { + local _file="$1" + local _checksum_value="$2" + local _calculated_checksum + + if [ -z "$_checksum_value" ]; then + return 0 + fi + + if ! check_cmd sha256sum; then + say "skipping sha256 checksum verification (requires 'sha256sum' command)" + return 0 + fi + _calculated_checksum="$(sha256sum -b "$_file" | awk '{printf $1}')" + + if [ "$_calculated_checksum" != "$_checksum_value" ]; then + err "checksum mismatch + want: $_checksum_value + got: $_calculated_checksum" + fi +} + +# Parse command line arguments +parse_args() { + for arg in "$@"; do + case "$arg" in + --version) + APP_VERSION="$2" + shift 2 + ;; + --verbose) + PRINT_VERBOSE=1 + ;; + --quiet) + PRINT_QUIET=0 + ;; + -h|--help) + usage + exit 0 + ;; + *) + err "Unknown argument: $1" + ;; + esac + done +} + +main() { + parse_args "$@" + + say "Starting installation..." + + check_dependencies + install_uv + setup_python + install_app + + say "Setup complete!" + say "You may need to restart your shell or run:" + say " export PATH=\"\$HOME/.local/bin:\$PATH\"" + say "" + say "Python $REQUIRED_PYTHON_VERSION, uv, and $APP_NAME are now installed." + + # Display any additional project-specific instructions here + say "" + say "To get started with $APP_NAME, try:" + say " $APP_NAME --help" +} + +main "$@" \ No newline at end of file diff --git a/install_test/linux/Dockerfile b/install_test/linux/Dockerfile new file mode 100644 index 00000000..05b0027f --- /dev/null +++ b/install_test/linux/Dockerfile @@ -0,0 +1,17 @@ +FROM debian:12 + +RUN apt-get update && apt-get upgrade -y + +RUN apt-get install -y \ + build-essential \ + curl + +WORKDIR /root + +COPY ./install.sh /root +RUN chmod +x /root/install.sh + +#CMD ["bash", "-c", "./install.sh; bash"] +CMD ["bash", "-c", "./install.sh --verbose; bash"] + +# docker build --no-cache -t debian -f install_test/linux/Dockerfile . && docker run -it debian \ No newline at end of file From 4c50c65e611760df9a2b34cbd335af0066b84a58 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Mon, 17 Feb 2025 16:52:54 -0800 Subject: [PATCH 02/22] Cleanup. --- install.sh | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/install.sh b/install.sh index deefdb17..89a07010 100644 --- a/install.sh +++ b/install.sh @@ -11,18 +11,6 @@ UV_INSTALLER_URL="https://astral.sh/uv/install.sh" PRINT_VERBOSE=0 PRINT_QUIET=0 -# workflow: -# do we have curl? if not, default to wget (TODO) -# install uv -# do we have a qualified python version? if not, install it with uv -# - do not modify the default `python` in the user's path -# store the path to the python version we installed for use later (ex: /usr/local/bin/python3.10) -# download the agentstack release -# install agentstack system-wide -# add agentstack to the user's path -# add agentstack to the user's shell config -# cleanup - say() { echo "$1" @@ -79,8 +67,9 @@ USAGE: OPTIONS: --version VERSION Specify version to install (default: latest) - --no-modify-path Don't modify shell PATH - -h, --help Show this help message + --verbose Enable verbose output + --quiet Suppress output + -h, --help Show this help message EOF } @@ -177,7 +166,7 @@ install_app() { say "Installing $APP_NAME..." local _zip_ext - _zip_ext=".tar.gz" # TODO detect this + _zip_ext=".tar.gz" # TODO do we need to fallback to .zip? local _url="$RELEASE_PATH_URL/$VERSION$_zip_ext" local _dir @@ -236,33 +225,29 @@ install_app() { esac # run setup - # TODO we should install to $HOME/.local/bin - # TODO if I install this system-wide with a specific python version, will - # setup py correctly configure it to use that python version after install? local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { err "Failed to find user site packages directory" } say_verbose "Installing to $_packages_dir" - # TODO silence output local _install_out _install_out="$(uv pip install --python=$PYTHON_BIN_PATH --target=$_packages_dir --directory=$_dir . 2>&1)" || { err "Failed to install $APP_NAME" } say_verbose "$_install_out" - make_bin "$PYTHON_BIN_PATH" "$HOME/.local/bin/$APP_NAME" + make_python_bin "$PYTHON_BIN_PATH" "$HOME/.local/bin/$APP_NAME" # verify installation ensure "$APP_NAME" --version # cleanup rm -rf "$_dir" - say "$APP_NAME installed successfully!" - # TODO retval } - -make_bin() { +# Manually create a bin file for the app +# $1: python bin path +# $2: program bin path +make_python_bin() { local _bin_content=$(cat < Date: Mon, 17 Feb 2025 17:57:36 -0800 Subject: [PATCH 03/22] Cleanup. --- install.sh | 125 ++++++++++++++++++---------------- install_test/linux/Dockerfile | 7 +- 2 files changed, 70 insertions(+), 62 deletions(-) diff --git a/install.sh b/install.sh index 89a07010..bd58ee56 100644 --- a/install.sh +++ b/install.sh @@ -1,6 +1,17 @@ #!/bin/bash set -e +LOGO=$(cat < /dev/null # cleanup rm -rf "$_dir" @@ -248,8 +267,10 @@ install_app() { # $1: python bin path # $2: program bin path make_python_bin() { + local _python_bin="$1" + local _program_bin="$2" local _bin_content=$(cat < $2 - chmod +x $2 + say_verbose "Creating bin file at $_program_bin" + echo "$_bin_content" > $_program_bin + chmod +x $_program_bin } # This wraps curl or wget. Try curl first, if not installed, use wget instead. -# $1: url -# $2: output file downloader() { + local _url="$1" + local _file="$2" local _cmd + if check_cmd curl; then - _cmd=curl + _cmd="curl -sSfL "$_url" -o "$_file"" elif check_cmd wget; then - _cmd=wget + _cmd="wget -q "$_url" -O "$_file"" else err "need curl or wget (command not found)" return 1 fi local _out - if [ "$_cmd" = curl ]; then - _out="$(curl -sSfL "$1" -o "$2" 2>&1)" || { - say_verbose "$_out" - return 1 - } - elif [ "$_cmd" = wget ]; then - _out="$(wget -q "$1" -O "$2" 2>&1)" || { - say_verbose "$_out" - return 1 - } - else - err "Unknown downloader" + local _out="$($_cmd 2>&1)" || { + say_verbose "$_out" return 1 - fi + } + return 0 } verify_sha256_checksum() { @@ -346,6 +359,8 @@ parse_args() { main() { parse_args "$@" + say "$LOGO" + say "" say "Starting installation..." check_dependencies @@ -353,16 +368,8 @@ main() { setup_python install_app - say "Setup complete!" - say "You may need to restart your shell or run:" - say " export PATH=\"\$HOME/.local/bin:\$PATH\"" - say "" - say "Python $REQUIRED_PYTHON_VERSION, uv, and $APP_NAME are now installed." - - # Display any additional project-specific instructions here say "" - say "To get started with $APP_NAME, try:" - say " $APP_NAME --help" + say "$MOTD" } main "$@" \ No newline at end of file diff --git a/install_test/linux/Dockerfile b/install_test/linux/Dockerfile index 05b0027f..ffdcc4ff 100644 --- a/install_test/linux/Dockerfile +++ b/install_test/linux/Dockerfile @@ -4,6 +4,7 @@ RUN apt-get update && apt-get upgrade -y RUN apt-get install -y \ build-essential \ + git \ curl WORKDIR /root @@ -11,7 +12,7 @@ WORKDIR /root COPY ./install.sh /root RUN chmod +x /root/install.sh -#CMD ["bash", "-c", "./install.sh; bash"] -CMD ["bash", "-c", "./install.sh --verbose; bash"] +CMD ["bash", "-c", "./install.sh; bash"] +#CMD ["bash", "-c", "./install.sh --verbose; bash"] -# docker build --no-cache -t debian -f install_test/linux/Dockerfile . && docker run -it debian \ No newline at end of file +# docker build -t debian -f install_test/linux/Dockerfile . && docker run -it debian \ No newline at end of file From f62cd43426a1aa20028dd85b889e9c03518829ec Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Mon, 17 Feb 2025 18:03:12 -0800 Subject: [PATCH 04/22] Allow version from cli argument. Fix print_quiet logic. --- install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index bd58ee56..e29ef4ef 100644 --- a/install.sh +++ b/install.sh @@ -20,7 +20,7 @@ REQUIRED_PYTHON_VERSION=">=3.10,<3.13" PYTHON_BIN_PATH="" UV_INSTALLER_URL="https://astral.sh/uv/install.sh" PRINT_VERBOSE=0 -PRINT_QUIET=0 +PRINT_QUIET=1 MOTD=$(cat < Date: Mon, 17 Feb 2025 18:18:36 -0800 Subject: [PATCH 05/22] Better formatting, better comments, note future improvements. --- install.sh | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/install.sh b/install.sh index e29ef4ef..924c1930 100644 --- a/install.sh +++ b/install.sh @@ -15,21 +15,18 @@ EOF APP_NAME="agentstack" VERSION="0.3.5" RELEASE_PATH_URL="https://github.com/AgentOps-AI/AgentStack/archive/refs/tags" -CHECKSUM_URL="" +CHECKSUM_URL="" # TODO REQUIRED_PYTHON_VERSION=">=3.10,<3.13" -PYTHON_BIN_PATH="" UV_INSTALLER_URL="https://astral.sh/uv/install.sh" +PYTHON_BIN_PATH="" PRINT_VERBOSE=0 PRINT_QUIET=1 MOTD=$(cat < /dev/null # cleanup rm -rf "$_dir" - say "$APP_NAME installed successfully!" + say "$APP_NAME $VERSION installed successfully!" } -# Manually create a bin file for the app -# $1: python bin path -# $2: program bin path +# Create a bin file for the app. Assumes entrypoint is main.py:main make_python_bin() { - local _python_bin="$1" - local _program_bin="$2" + local _program_bin="$1" local _bin_content=$(cat < Date: Tue, 18 Feb 2025 09:28:38 -0800 Subject: [PATCH 06/22] Properly handle uv error exits where exit code is still 0. --- install.sh | 97 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 25 deletions(-) diff --git a/install.sh b/install.sh index 924c1930..f02856f6 100644 --- a/install.sh +++ b/install.sh @@ -67,16 +67,21 @@ say_verbose() { } err() { - if [ "0" = "$PRINT_QUIET" ]; then - local red - local reset - red=$(tput setaf 1 2>/dev/null || echo '') - reset=$(tput sgr0 2>/dev/null || echo '') - say "${red}ERROR${reset}: $1" >&2 + if [ "1" = "$PRINT_QUIET" ]; then + local _red=$(tput setaf 1 2>/dev/null || echo '') + local _reset=$(tput sgr0 2>/dev/null || echo '') + say "${_red}[ERROR]${_reset}: $1" >&2 + say "Run install with --verbose for more details." fi exit 1 } +# Check if a command exists +check_cmd() { + command -v "$1" > /dev/null 2>&1 + return $? +} + # Check if a command exists and print an error message if it doesn't need_cmd() { if ! check_cmd "$1"; then @@ -85,10 +90,19 @@ need_cmd() { fi } -# Check if a command exists -check_cmd() { - command -v "$1" > /dev/null 2>&1 - return $? +# Check if one of multiple commands exist and print an error message if none do +need_cmds() { + local _found=0 + for cmd in "$@"; do + if check_cmd "$cmd"; then + _found=1 + break + fi + done + + if [ $_found -eq 0 ]; then + err "need one of: $* (command not found)" + fi } # Run a command that should never fail. If the command fails execution @@ -103,13 +117,14 @@ check_dependencies() { need_cmd mktemp need_cmd chmod need_cmd rm - need_cmd tar need_cmd grep need_cmd awk need_cmd cat + need_cmds curl wget + need_cmds tar unzip # need gcc to install psutil which is a sub-dependency of agentstack - need_cmd gcc + #need_cmd gcc } # Install uv @@ -121,7 +136,7 @@ install_uv() { say "Installing uv..." fi - # determine which downloader to use + # determine which download_file to use local _install_cmd if check_cmd curl; then say_verbose "Running uv installer with curl" @@ -134,6 +149,7 @@ install_uv() { fi # run the installer + say_verbose "$_install_cmd" local _output=$(eval "$_install_cmd" 2>&1) local _retval=$? say_verbose "$_output" @@ -144,7 +160,7 @@ install_uv() { # ensure uv is in PATH if ! check_cmd uv; then say_verbose "Adding ~/.local/bin to PATH" - export PATH="$HOME/.local/bin:$PATH" + update_path "$HOME/.local/bin" fi # verify uv installation @@ -205,9 +221,9 @@ install_app() { ensure mkdir -p "$_dir" # download tar or zip - if ! downloader "$_url" "$_file"; then + if ! download_file "$_url" "$_file"; then say_verbose "failed to download $_url" - sy "Failed to download $APP_NAME $VERSION" + say "Failed to download $APP_NAME $VERSION" say "(this may be a standard network error, but it may also indicate" say "that $APP_NAME's release process is not working. When in doubt" say "please feel free to open an issue!)" @@ -215,7 +231,7 @@ install_app() { fi # download checksum - if ! downloader "$CHECKSUM_URL" "$_checksum_file"; then + if ! download_file "$CHECKSUM_URL" "$_checksum_file"; then say_verbose "failed to download checksum file: $CHECKSUM_URL" say "Skipping checksum verification" fi @@ -224,11 +240,10 @@ install_app() { # github action generates checksums in the following format: # 0.3.4.tar.gz ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb # 0.3.4.zip 0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f - if [ -e $_checksum_file ]; then + if [ -f $_checksum_file ]; then # TODO this needs to be tested. say_verbose "verifying checksum" - local _all_checksums="$(cat "$_checksum_file")" - local _checksum_value="$(echo "$_all_checksums" | grep "$VERSION$_zip_ext" | awk '{print $1}')" + local _checksum_value="$(cat "$_checksum_file" | grep "${VERSION}${_zip_ext}" | awk '{print $2}')" verify_sha256_checksum "$_file" "$_checksum_value" fi @@ -250,12 +265,16 @@ install_app() { err "Failed to find user site packages directory" } say_verbose "Installing to $_packages_dir" - local _install_out - _install_out="$(uv pip install --python=$PYTHON_BIN_PATH --target=$_packages_dir --directory=$_dir . 2>&1)" || { - err "Failed to install $APP_NAME" - } + local _install_cmd="uv pip install --python="$PYTHON_BIN_PATH" --target="$_packages_dir" --directory="$_dir" ." + say_verbose "$_install_cmd" + local _install_out="$(eval "$_install_cmd" 2>&1)" say_verbose "$_install_out" + if [ $? -ne 0 ] || echo "$_install_out" | grep -qi "error\|failed\|exception"; then + err "Failed to install $APP_NAME." + fi + make_python_bin "$HOME/.local/bin/$APP_NAME" + say_verbose "Added bin to ~/.local/bin/$APP_NAME" # verify installation ensure "$APP_NAME" --version > /dev/null @@ -265,6 +284,34 @@ install_app() { say "$APP_NAME $VERSION installed successfully!" } +update_path() { + local new_path="$1" + + # early exit if path is already present + case ":$PATH:" in + *":$new_path:"*) return 0 ;; + esac + + # update for current session + export PATH="$new_path:$PATH" + + local config_files=( + "$HOME/.bashrc" # bash + "$HOME/.zshrc" # ssh + "$HOME/.profile" # POSIX fallback (sh, ksh, etc.) + ) + + # update for each shell + for config_file in "${config_files[@]}"; do + if [ -f "$config_file" ]; then + if ! grep -E "^[^#]*export[[:space:]]+PATH=.*(:$new_path|$new_path:|$new_path\$)" "$config_file" >/dev/null 2>&1; then + echo "export PATH=\"$new_path:\$PATH\"" >> "$config_file" + say_verbose "Added $new_path to $config_file" + fi + fi + done +} + # Create a bin file for the app. Assumes entrypoint is main.py:main make_python_bin() { local _program_bin="$1" @@ -286,7 +333,7 @@ EOF } # Download a file. Try curl first, if not installed, use wget instead. -downloader() { +download_file() { local _url="$1" local _file="$2" local _cmd From b8c86f7fc69c4862a4156f258ad68d58650edf00 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 09:29:01 -0800 Subject: [PATCH 07/22] GitHub action for calculating checksums on release. --- .github/workflows/release-checksum.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/release-checksum.yml diff --git a/.github/workflows/release-checksum.yml b/.github/workflows/release-checksum.yml new file mode 100644 index 00000000..9fe530a8 --- /dev/null +++ b/.github/workflows/release-checksum.yml @@ -0,0 +1,23 @@ +name: Create checksum.txt for releases + +on: + release: + types: [created] + workflow_dispatch: + +jobs: + test: + runs-on: macos-latest + + steps: + - name: Run checksum action + uses: thewh1teagle/checksum@v1 + with: + patterns: | + *.zip + *.tar.gz + algorithm: sha256 + dry-run: checksum.txt + env: + # You must enable write permission in github.com/user/repo/settings/actions -> Workflow permissions -> Read and write permissions + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 6743a1bfd3f76178e0fd5c3618e11909b3b22313 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 12:44:50 -0800 Subject: [PATCH 08/22] Allow installation from dev branches. Better error messaging. --- agentstack/update.py | 1 + install.sh | 192 ++++++++++++++---- .../install_script}/linux/Dockerfile | 14 +- 3 files changed, 160 insertions(+), 47 deletions(-) rename {install_test => tests/install_script}/linux/Dockerfile (50%) diff --git a/agentstack/update.py b/agentstack/update.py index 2787e3d4..e6044782 100644 --- a/agentstack/update.py +++ b/agentstack/update.py @@ -113,6 +113,7 @@ def check_for_updates(update_requested: bool = False): if inquirer.confirm( f"New version of {AGENTSTACK_PACKAGE} available: {latest_version}! Do you want to install?" ): + # TODO handle update for system version of agentstack packaging.upgrade(f'{AGENTSTACK_PACKAGE}[{get_framework()}]') log.success(f"{AGENTSTACK_PACKAGE} updated. Re-run your command to use the latest version.") else: diff --git a/install.sh b/install.sh index f02856f6..26991ec4 100644 --- a/install.sh +++ b/install.sh @@ -14,11 +14,13 @@ EOF APP_NAME="agentstack" VERSION="0.3.5" -RELEASE_PATH_URL="https://github.com/AgentOps-AI/AgentStack/archive/refs/tags" +REPO_URL="https://github.com/AgentOps-AI/AgentStack" +RELEASE_PATH_URL="$REPO_URL/archive/refs/tags" CHECKSUM_URL="" # TODO REQUIRED_PYTHON_VERSION=">=3.10,<3.13" UV_INSTALLER_URL="https://astral.sh/uv/install.sh" PYTHON_BIN_PATH="" +DEV_BRANCH="" PRINT_VERBOSE=0 PRINT_QUIET=1 @@ -43,26 +45,26 @@ USAGE: agentstack-install.sh [OPTIONS] OPTIONS: - --version VERSION Specify version to install (default: latest) - --verbose Enable verbose output - --quiet Suppress output - -h, --help Show this help message + --version= Specify version to install (default: $VERSION) + --dev-branch= Install from a specific git branch/commit/tag + --verbose Enable verbose output + --quiet Suppress output + -h, --help Show this help message EOF } # TODO allow user to specify install path with --target # TODO allow user to specify Python version with --python-version -# TODO install `apt install build-essential` if not installed -# is gcc preinstalled on MacOS? +# TODO allow installing a specific branch/commit/tag with --dev-branch say() { if [ "1" = "$PRINT_QUIET" ]; then - echo "$1" + echo -e "$1" fi } say_verbose() { if [ "1" = "$PRINT_VERBOSE" ]; then - echo "[DEBUG] $1" + echo -e "[DEBUG] $1" fi } @@ -70,12 +72,29 @@ err() { if [ "1" = "$PRINT_QUIET" ]; then local _red=$(tput setaf 1 2>/dev/null || echo '') local _reset=$(tput sgr0 2>/dev/null || echo '') - say "${_red}[ERROR]${_reset}: $1" >&2 - say "Run install with --verbose for more details." + say "\n${_red}[ERROR]${_reset}: $1" >&2 + say "\nRun with --verbose for more details." fi exit 1 } +err_missing_cmd() { + local _cmd_name=$1 + local _help_text="" + local _platform=$(platform) + + if [ $_platform == "linux" ]; then + if [ $_cmd_name == "gcc" ]; then + _help_text="Hint: sudo apt-get install build-essential" + else + _help_text="Hint: sudo apt-get install $_cmd_name" + fi + elif [ $_platform == "macos" ]; then + _help_text="Hint: brew install $_cmd_name" + fi + err "A required dependency is missing. Please install: $*\n$_help_text" +} + # Check if a command exists check_cmd() { command -v "$1" > /dev/null 2>&1 @@ -85,13 +104,12 @@ check_cmd() { # Check if a command exists and print an error message if it doesn't need_cmd() { if ! check_cmd "$1"; then - err "need '$1' (command not found)" - # TODO more helpful error message based on platform + err_missing_cmd $1 fi } # Check if one of multiple commands exist and print an error message if none do -need_cmds() { +need_cmd_option() { local _found=0 for cmd in "$@"; do if check_cmd "$cmd"; then @@ -101,18 +119,26 @@ need_cmds() { done if [ $_found -eq 0 ]; then - err "need one of: $* (command not found)" + err_missing_cmd $1 fi } -# Run a command that should never fail. If the command fails execution -# will immediately terminate with an error showing the failing command. ensure() { if ! "$@"; then err "command failed: $*"; fi } +platform() { + case "$(uname -s)" in + Linux*) echo "linux" ;; + Darwin*) echo "macos" ;; + CYGWIN*) echo "cygwin" ;; + *) echo "unknown" ;; + esac +} + # Check for required commands check_dependencies() { + say "Checking dependencies..." need_cmd mkdir need_cmd mktemp need_cmd chmod @@ -121,10 +147,10 @@ check_dependencies() { need_cmd awk need_cmd cat - need_cmds curl wget - need_cmds tar unzip - # need gcc to install psutil which is a sub-dependency of agentstack - #need_cmd gcc + need_cmd_option curl wget + need_cmd_option tar unzip + need_cmd gcc # need gcc to install psutil + say "Dependencies are met." } # Install uv @@ -136,7 +162,7 @@ install_uv() { say "Installing uv..." fi - # determine which download_file to use + # download with curl or wget local _install_cmd if check_cmd curl; then say_verbose "Running uv installer with curl" @@ -145,7 +171,7 @@ install_uv() { say_verbose "Running uv installer with wget" _install_cmd="wget -qO- $UV_INSTALLER_URL | sh" else - err "Neither curl nor wget is available. Please install one of them." + err "neither curl nor wget is available" fi # run the installer @@ -169,7 +195,7 @@ install_uv() { _uv_version=0 } if [ -z "$_uv_version" ]; then - err "uv installation failed. Please ensure ~/.local/bin is in your PATH" + err "uv installation failed." else say "$_uv_version installed successfully!" fi @@ -181,7 +207,8 @@ setup_python() { PYTHON_BIN_PATH="" } if [ -x "$PYTHON_BIN_PATH" ]; then - say "Python $REQUIRED_PYTHON_VERSION is already installed." + local _python_version="$($PYTHON_BIN_PATH --version 2>&1)" + say "Python $_python_version is available." return 0 else say "Installing Python $REQUIRED_PYTHON_VERSION..." @@ -197,20 +224,25 @@ setup_python() { local _python_version="$($PYTHON_BIN_PATH --version 2>&1)" say "Python $_python_version installed successfully!" else - err "Failed to install Python $REQUIRED_PYTHON_VERSION" + err "Failed to install Python" fi } -# Install the application -install_app() { +# Install an official release of the app +install_release() { say "Installing $APP_NAME..." local _zip_ext - _zip_ext=".tar.gz" # TODO do we need to fallback to .zip? + if check_cmd tar; then + _zip_ext=".tar.gz" + elif check_cmd unzip; then + _zip_ext=".zip" + else + err "could not find tar or unzip" + fi - local _url="$RELEASE_PATH_URL/$VERSION$_zip_ext" - local _dir - _dir="$(ensure mktemp -d)" || return 1 + local _url="${RELEASE_PATH_URL}/${VERSION}${_zip_ext}" + local _dir="$(ensure mktemp -d)" || return 1 local _file="$_dir/input$_zip_ext" local _checksum_file="$_dir/checksum" @@ -218,8 +250,6 @@ install_app() { say_verbose " from $_url" 1>&2 say_verbose " to $_file" 1>&2 - ensure mkdir -p "$_dir" - # download tar or zip if ! download_file "$_url" "$_file"; then say_verbose "failed to download $_url" @@ -256,11 +286,49 @@ install_app() { ensure tar xf "$_file" --strip-components 1 -C "$_dir" ;; *) - err "unknown archive format: $_zip_ext" + err "unknown archive format" ;; esac - # run setup + setup_app "$_dir" + + # cleanup + rm -rf "$_dir" + say "$APP_NAME $VERSION installed successfully!" +} + +install_dev_branch() { + need_cmd git + if [ -z "$DEV_BRANCH" ]; then + err "DEV_BRANCH is not set" + fi + + say "Installing $APP_NAME..." + local _dir="$(ensure mktemp -d)" || return 1 + + # clone from git + local _git_url="$REPO_URL.git" + local _git_cmd="git clone --depth 1 $_git_url $_dir" + say_verbose "$_git_cmd" + local _git_out="$($_git_cmd 2>&1)" + say_verbose "$_git_out" + if [ $? -ne 0 ] || echo "$_git_out" | grep -qi "error\|fatal"; then + err "Failed to clone git repo." + fi + + # checkout + local _tag=${DEV_BRANCH#*:} # just the tag name (pull/123/head:pr-123 -> pr-123) + ensure git -C $_dir fetch origin $DEV_BRANCH + ensure git -C $_dir checkout $_tag + setup_app "$_dir" + + # cleanup + rm -rf "$_dir" + say "$APP_NAME @ $DEV_BRANCH installed successfully!" +} + +setup_app() { + local _dir="$1" local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { err "Failed to find user site packages directory" } @@ -278,10 +346,6 @@ install_app() { # verify installation ensure "$APP_NAME" --version > /dev/null - - # cleanup - rm -rf "$_dir" - say "$APP_NAME $VERSION installed successfully!" } update_path() { @@ -380,24 +444,60 @@ verify_sha256_checksum() { } parse_args() { - for arg in "$@"; do - case "$arg" in + while [[ $# -gt 0 ]]; do + case "$1" in + --version=*) + VERSION="${1#*=}" + shift + ;; --version) + if [[ -z "$2" || "$2" == -* ]]; then + err "Error: --version requires a value" + usage + exit 1 + fi VERSION="$2" shift 2 ;; + --dev-branch=*) + DEV_BRANCH="${1#*=}" + shift + ;; + --dev-branch) + if [[ -z "$2" || "$2" == -* ]]; then + err "Error: --dev-branch requires a value" + usage + exit 1 + fi + DEV_BRANCH="$2" + shift 2 + ;; --verbose) PRINT_VERBOSE=1 + shift ;; --quiet) PRINT_QUIET=0 + shift ;; -h|--help) usage exit 0 ;; + -*) + err "Unknown option: $1" + usage + exit 1 + ;; *) - err "Unknown argument: $1" + if [[ -z "$COMMAND" ]]; then + COMMAND="$1" + else + err "Unexpected argument: $1" + usage + exit 1 + fi + shift ;; esac done @@ -413,7 +513,11 @@ main() { check_dependencies install_uv setup_python - install_app + if [ -n "$DEV_BRANCH" ]; then + install_dev_branch + else + install_release + fi say "" say "$MOTD" diff --git a/install_test/linux/Dockerfile b/tests/install_script/linux/Dockerfile similarity index 50% rename from install_test/linux/Dockerfile rename to tests/install_script/linux/Dockerfile index ffdcc4ff..ee55e26e 100644 --- a/install_test/linux/Dockerfile +++ b/tests/install_script/linux/Dockerfile @@ -4,15 +4,23 @@ RUN apt-get update && apt-get upgrade -y RUN apt-get install -y \ build-essential \ - git \ - curl + git \ + # \ + #curl + wget + +# build-essential \ + WORKDIR /root COPY ./install.sh /root RUN chmod +x /root/install.sh -CMD ["bash", "-c", "./install.sh; bash"] +#CMD ["bash", "-c", "./install.sh; bash"] #CMD ["bash", "-c", "./install.sh --verbose; bash"] +#CMD ["bash", "-c", "./install.sh --version=\"0.3.4\"; bash"] +#CMD ["bash", "-c", "./install.sh --dev-branch=main; bash"] +CMD ["bash", "-c", "./install.sh --dev-branch=\"pull/318/head:pr-318\"; bash"] # docker build -t debian -f install_test/linux/Dockerfile . && docker run -it debian \ No newline at end of file From f028edefb43621ca63f7c7ead25ed7086a8c9087 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 13:08:07 -0800 Subject: [PATCH 09/22] Upgrade system version and user project versions. Check for updates every 12 hours. --- agentstack/conf.py | 6 +++++- agentstack/packaging.py | 42 +++++++++++++++++++++++++---------------- agentstack/update.py | 15 +++++++++++---- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/agentstack/conf.py b/agentstack/conf.py index 64319f4b..fe0e6432 100644 --- a/agentstack/conf.py +++ b/agentstack/conf.py @@ -15,12 +15,16 @@ PATH: Path = Path() +class NoProjectError(Exception): + pass + + def assert_project() -> None: try: ConfigFile() return except FileNotFoundError: - raise Exception("Could not find agentstack.json, are you in an AgentStack project directory?") + raise NoProjectError("Could not find agentstack.json, are you in an AgentStack project directory?") def set_path(path: Union[str, Path, None]): diff --git a/agentstack/packaging.py b/agentstack/packaging.py index e226db96..4ef66530 100644 --- a/agentstack/packaging.py +++ b/agentstack/packaging.py @@ -20,10 +20,17 @@ # In testing, when this was not set, packages could end up in the pyenv's # site-packages directory; it's possible an environment variable can control this. +_python_executable = ".venv/bin/python" + +def set_python_executable(path: str): + global _python_executable + + _python_executable = path + def install(package: str): """Install a package with `uv` and add it to pyproject.toml.""" - + global _python_executable from agentstack.cli.spinner import Spinner def on_progress(line: str): @@ -35,7 +42,7 @@ def on_error(line: str): with Spinner(f"Installing {package}") as spinner: _wrap_command_with_callbacks( - [get_uv_bin(), 'add', '--python', '.venv/bin/python', package], + [get_uv_bin(), 'add', '--python', _python_executable, package], on_progress=on_progress, on_error=on_error, ) @@ -43,7 +50,7 @@ def on_error(line: str): def install_project(): """Install all dependencies for the user's project.""" - + global _python_executable from agentstack.cli.spinner import Spinner def on_progress(line: str): @@ -56,14 +63,14 @@ def on_error(line: str): try: with Spinner(f"Installing project dependencies.") as spinner: result = _wrap_command_with_callbacks( - [get_uv_bin(), 'pip', 'install', '--python', '.venv/bin/python', '.'], + [get_uv_bin(), 'pip', 'install', '--python', _python_executable, '.'], on_progress=on_progress, on_error=on_error, ) if result is False: spinner.clear_and_log("Retrying uv installation with --no-cache flag...", 'info') _wrap_command_with_callbacks( - [get_uv_bin(), 'pip', 'install', '--no-cache', '--python', '.venv/bin/python', '.'], + [get_uv_bin(), 'pip', 'install', '--no-cache', '--python', _python_executable, '.'], on_progress=on_progress, on_error=on_error, ) @@ -87,13 +94,13 @@ def on_error(line: str): log.info(f"Uninstalling {requirement.name}") _wrap_command_with_callbacks( - [get_uv_bin(), 'remove', '--python', '.venv/bin/python', requirement.name], + [get_uv_bin(), 'remove', '--python', _python_executable, requirement.name], on_progress=on_progress, on_error=on_error, ) -def upgrade(package: str): +def upgrade(package: str, use_venv: bool = True): """Upgrade a package with `uv`.""" # TODO should we try to update the project's pyproject.toml as well? @@ -106,9 +113,10 @@ def on_error(line: str): log.info(f"Upgrading {package}") _wrap_command_with_callbacks( - [get_uv_bin(), 'pip', 'install', '-U', '--python', '.venv/bin/python', package], + [get_uv_bin(), 'pip', 'install', '-U', '--python', _python_executable, package], on_progress=on_progress, on_error=on_error, + use_venv=use_venv, ) @@ -156,19 +164,21 @@ def _wrap_command_with_callbacks( on_progress: Callable[[str], None] = lambda x: None, on_complete: Callable[[str], None] = lambda x: None, on_error: Callable[[str], None] = lambda x: None, + use_venv: bool = True, ) -> bool: """Run a command with progress callbacks. Returns bool for cmd success.""" process = None try: all_lines = '' - process = subprocess.Popen( - command, - cwd=conf.PATH.absolute(), - env=_setup_env(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) + sub_args = { + 'cwd': conf.PATH.absolute(), + 'stdout': subprocess.PIPE, + 'stderr': subprocess.PIPE, + 'text': True, + } + if use_venv: + sub_args['env'] = _setup_env() + process = subprocess.Popen(command, **sub_args) assert process.stdout and process.stderr # appease type checker readable = [process.stdout, process.stderr] diff --git a/agentstack/update.py b/agentstack/update.py index e6044782..c1f9eae8 100644 --- a/agentstack/update.py +++ b/agentstack/update.py @@ -4,7 +4,7 @@ from pathlib import Path from packaging.version import parse as parse_version, Version import inquirer -from agentstack import log +from agentstack import conf, log from agentstack.utils import term_color, get_version, get_framework, get_base_dir from agentstack import packaging @@ -24,7 +24,7 @@ USER_GUID_FILE_PATH = get_base_dir() / ".cli-user-guid" INSTALL_PATH = Path(sys.executable).parent.parent ENDPOINT_URL = "https://pypi.org/simple" -CHECK_EVERY = 3600 # hour +CHECK_EVERY = 12 * 60 * 60 # 12 hours def _is_ci_environment(): @@ -113,8 +113,15 @@ def check_for_updates(update_requested: bool = False): if inquirer.confirm( f"New version of {AGENTSTACK_PACKAGE} available: {latest_version}! Do you want to install?" ): - # TODO handle update for system version of agentstack - packaging.upgrade(f'{AGENTSTACK_PACKAGE}[{get_framework()}]') + try: + # handle update inside a user project + conf.assert_project() + packaging.upgrade(f'{AGENTSTACK_PACKAGE}[{get_framework()}]') + except conf.NoProjectError: + # handle update for system version of agentstack + packaging.set_python_executable(sys.executable) + packaging.upgrade(AGENTSTACK_PACKAGE, use_venv=False) + log.success(f"{AGENTSTACK_PACKAGE} updated. Re-run your command to use the latest version.") else: log.info("Skipping update. Run `agentstack update` to install the latest version.") From df9c1ee9b8419489a6df1774d32d379a27c4efd9 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 13:08:42 -0800 Subject: [PATCH 10/22] Exit installer if already installed. --- install.sh | 6 ++++++ tests/install_script/linux/Dockerfile | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 26991ec4..704fba0d 100644 --- a/install.sh +++ b/install.sh @@ -510,6 +510,12 @@ main() { say "" say "Starting installation..." + if check_cmd $APP_NAME; then + say "AgentStack is already installed." + say "Run 'agentstack update' to update to the latest version." + exit 0 + fi + check_dependencies install_uv setup_python diff --git a/tests/install_script/linux/Dockerfile b/tests/install_script/linux/Dockerfile index ee55e26e..b426d5a5 100644 --- a/tests/install_script/linux/Dockerfile +++ b/tests/install_script/linux/Dockerfile @@ -23,4 +23,4 @@ RUN chmod +x /root/install.sh #CMD ["bash", "-c", "./install.sh --dev-branch=main; bash"] CMD ["bash", "-c", "./install.sh --dev-branch=\"pull/318/head:pr-318\"; bash"] -# docker build -t debian -f install_test/linux/Dockerfile . && docker run -it debian \ No newline at end of file +# docker build -t debian -f tests/install_script/linux/Dockerfile . && docker run -it debian \ No newline at end of file From 9e5d61f031c0ad0c95f64f45d439a602caef44b8 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 13:21:27 -0800 Subject: [PATCH 11/22] Specify target so uv will install in user site-packages. --- agentstack/packaging.py | 8 +++++++- install.sh | 10 ++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/agentstack/packaging.py b/agentstack/packaging.py index 4ef66530..3e6dfe55 100644 --- a/agentstack/packaging.py +++ b/agentstack/packaging.py @@ -4,6 +4,7 @@ import re import subprocess import select +import site from packaging.requirements import Requirement from agentstack import conf, log @@ -111,9 +112,14 @@ def on_progress(line: str): def on_error(line: str): log.error(f"uv: [error]\n {line.strip()}") + extra_args = [] + if not use_venv: + # uv won't let us install without a venv if we don't specify a target + extra_args = ['--target', site.getusersitepackages()] + log.info(f"Upgrading {package}") _wrap_command_with_callbacks( - [get_uv_bin(), 'pip', 'install', '-U', '--python', _python_executable, package], + [get_uv_bin(), 'pip', 'install', '-U', '--python', _python_executable, *extra_args, package], on_progress=on_progress, on_error=on_error, use_venv=use_venv, diff --git a/install.sh b/install.sh index 704fba0d..0f63f948 100644 --- a/install.sh +++ b/install.sh @@ -290,9 +290,8 @@ install_release() { ;; esac + # install & cleanup setup_app "$_dir" - - # cleanup rm -rf "$_dir" say "$APP_NAME $VERSION installed successfully!" } @@ -320,14 +319,17 @@ install_dev_branch() { local _tag=${DEV_BRANCH#*:} # just the tag name (pull/123/head:pr-123 -> pr-123) ensure git -C $_dir fetch origin $DEV_BRANCH ensure git -C $_dir checkout $_tag - setup_app "$_dir" + - # cleanup + # install & cleanup + setup_app "$_dir" rm -rf "$_dir" say "$APP_NAME @ $DEV_BRANCH installed successfully!" } setup_app() { + return 0 # temp + local _dir="$1" local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { err "Failed to find user site packages directory" From 75e5fdd6fc9777a5d0b66f761135072a1887f459 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 13:28:29 -0800 Subject: [PATCH 12/22] Decrement version for testing; I can't figure out a better way to do this. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5c9f2aa6..d82dcc86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentstack" -version = "0.3.5" +version = "0.3.4" description = "The fastest way to build robust AI agents" authors = [ { name="Braelyn Boynton", email="bboynton97@gmail.com" }, From 6ca050c3705c96932b315bcc36bc547ba3100ae2 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 13:29:06 -0800 Subject: [PATCH 13/22] Re-enable setup_app --- install.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/install.sh b/install.sh index 0f63f948..e54e0485 100644 --- a/install.sh +++ b/install.sh @@ -328,8 +328,6 @@ install_dev_branch() { } setup_app() { - return 0 # temp - local _dir="$1" local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { err "Failed to find user site packages directory" From 2b772fbc85e629cbd3e2f1f921607abe4e0a3ac9 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 13:31:05 -0800 Subject: [PATCH 14/22] Restore version number. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d82dcc86..5c9f2aa6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "agentstack" -version = "0.3.4" +version = "0.3.5" description = "The fastest way to build robust AI agents" authors = [ { name="Braelyn Boynton", email="bboynton97@gmail.com" }, From 30fc1f27f53d0bf120464ca75fc4c302d3ffc0ab Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 13:33:36 -0800 Subject: [PATCH 15/22] Wack type error. --- agentstack/packaging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agentstack/packaging.py b/agentstack/packaging.py index 3e6dfe55..73d5fb98 100644 --- a/agentstack/packaging.py +++ b/agentstack/packaging.py @@ -184,7 +184,7 @@ def _wrap_command_with_callbacks( } if use_venv: sub_args['env'] = _setup_env() - process = subprocess.Popen(command, **sub_args) + process = subprocess.Popen(command, **sub_args) # type: ignore assert process.stdout and process.stderr # appease type checker readable = [process.stdout, process.stderr] From a6c8f8d2553b62b8ed201470e28bbe9b4b7873df Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 16:17:56 -0800 Subject: [PATCH 16/22] Show activity during installs. --- install.sh | 76 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 21 deletions(-) diff --git a/install.sh b/install.sh index e54e0485..7bdc17f3 100644 --- a/install.sh +++ b/install.sh @@ -19,16 +19,25 @@ RELEASE_PATH_URL="$REPO_URL/archive/refs/tags" CHECKSUM_URL="" # TODO REQUIRED_PYTHON_VERSION=">=3.10,<3.13" UV_INSTALLER_URL="https://astral.sh/uv/install.sh" -PYTHON_BIN_PATH="" -DEV_BRANCH="" +PYTHON_BIN_PATH="" # set after a verified install is found +DEV_BRANCH="" # set by --dev-branch flag PRINT_VERBOSE=0 PRINT_QUIET=1 -MOTD=$(cat </dev/null || echo '') local _reset=$(tput sgr0 2>/dev/null || echo '') say "\n${_red}[ERROR]${_reset}: $1" >&2 say "\nRun with --verbose for more details." + say "\nIf you need help, please feel free to open an issue:" + say "\n $REPO_URL/issues\n" fi exit 1 } @@ -161,6 +178,8 @@ install_uv() { else say "Installing uv..." fi + show_activity & + local _activity_pid=$! # download with curl or wget local _install_cmd @@ -194,6 +213,10 @@ install_uv() { _uv_version="$(uv --version 2>/dev/null)" || { _uv_version=0 } + + kill $_activity_pid + say "" + if [ -z "$_uv_version" ]; then err "uv installation failed." else @@ -211,6 +234,9 @@ setup_python() { say "Python $_python_version is available." return 0 else + show_activity & + local _activity_pid=$! + say "Installing Python $REQUIRED_PYTHON_VERSION..." uv python install "$REQUIRED_PYTHON_VERSION" --preview 2>/dev/null || { err "Failed to install Python" @@ -218,8 +244,11 @@ setup_python() { PYTHON_BIN_PATH="$(uv python find "$REQUIRED_PYTHON_VERSION")" || { err "Failed to find Python" } + + kill $_activity_pid + say "" fi - + if [ -x "$PYTHON_BIN_PATH" ]; then local _python_version="$($PYTHON_BIN_PATH --version 2>&1)" say "Python $_python_version installed successfully!" @@ -231,7 +260,9 @@ setup_python() { # Install an official release of the app install_release() { say "Installing $APP_NAME..." - + show_activity & + local _activity_pid=$! + local _zip_ext if check_cmd tar; then _zip_ext=".tar.gz" @@ -253,11 +284,7 @@ install_release() { # download tar or zip if ! download_file "$_url" "$_file"; then say_verbose "failed to download $_url" - say "Failed to download $APP_NAME $VERSION" - say "(this may be a standard network error, but it may also indicate" - say "that $APP_NAME's release process is not working. When in doubt" - say "please feel free to open an issue!)" - exit 1 + err "Failed to download $APP_NAME $VERSION" fi # download checksum @@ -293,9 +320,13 @@ install_release() { # install & cleanup setup_app "$_dir" rm -rf "$_dir" + + kill $_activity_pid + say "" say "$APP_NAME $VERSION installed successfully!" } +# Install a specific branch/commit/tag from the git repo install_dev_branch() { need_cmd git if [ -z "$DEV_BRANCH" ]; then @@ -303,6 +334,8 @@ install_dev_branch() { fi say "Installing $APP_NAME..." + show_activity & + local _activity_pid=$! local _dir="$(ensure mktemp -d)" || return 1 # clone from git @@ -319,14 +352,17 @@ install_dev_branch() { local _tag=${DEV_BRANCH#*:} # just the tag name (pull/123/head:pr-123 -> pr-123) ensure git -C $_dir fetch origin $DEV_BRANCH ensure git -C $_dir checkout $_tag - # install & cleanup setup_app "$_dir" rm -rf "$_dir" + + kill $_activity_pid + say "" say "$APP_NAME @ $DEV_BRANCH installed successfully!" } +# Install the app in the user's site-packages directory and add a executable setup_app() { local _dir="$1" local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { @@ -348,6 +384,7 @@ setup_app() { ensure "$APP_NAME" --version > /dev/null } +# Update PATH in shell config files update_path() { local new_path="$1" @@ -506,13 +543,11 @@ parse_args() { main() { parse_args "$@" - say "$LOGO" - say "" + say "$LOGO\n" say "Starting installation..." if check_cmd $APP_NAME; then - say "AgentStack is already installed." - say "Run 'agentstack update' to update to the latest version." + say "\n$MSG_ALREADY_INSTALLED\n" exit 0 fi @@ -525,9 +560,8 @@ main() { install_release fi - say "" - say "$MOTD" - say "" + say "\n$MSG_SUCCESS\n" + exit 0 } main "$@" \ No newline at end of file From ef67d5c3a7ecacd6323597f257cd36601fc756e2 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 18:44:50 -0800 Subject: [PATCH 17/22] Tests for install script. --- install.sh | 44 +++++++---- tests/install_script/linux/Dockerfile | 26 ------ tests/install_script/run_tests.py | 109 ++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 41 deletions(-) delete mode 100644 tests/install_script/linux/Dockerfile create mode 100644 tests/install_script/run_tests.py diff --git a/install.sh b/install.sh index 7bdc17f3..a28d815b 100644 --- a/install.sh +++ b/install.sh @@ -17,7 +17,7 @@ VERSION="0.3.5" REPO_URL="https://github.com/AgentOps-AI/AgentStack" RELEASE_PATH_URL="$REPO_URL/archive/refs/tags" CHECKSUM_URL="" # TODO -REQUIRED_PYTHON_VERSION=">=3.10,<3.13" +PYTHON_VERSION=">=3.10,<3.13" UV_INSTALLER_URL="https://astral.sh/uv/install.sh" PYTHON_BIN_PATH="" # set after a verified install is found DEV_BRANCH="" # set by --dev-branch flag @@ -48,17 +48,18 @@ agentstack-install.sh The installer for AgentStack This script installs uv the Python package manager, installs a compatible Python -version ($REQUIRED_PYTHON_VERSION), and installs AgentStack. +version ($PYTHON_VERSION), and installs AgentStack. USAGE: agentstack-install.sh [OPTIONS] OPTIONS: - --version= Specify version to install (default: $VERSION) - --dev-branch= Install from a specific git branch/commit/tag - --verbose Enable verbose output - --quiet Suppress output - -h, --help Show this help message + --version= Specify version to install (default: $VERSION) + --python-version= Specify Python version to install (default: $PYTHON_VERSION) + --dev-branch= Install from a specific git branch/commit/tag + --verbose Enable verbose output + --quiet Suppress output + -h, --help Show this help message EOF } # TODO allow user to specify install path with --target @@ -220,13 +221,13 @@ install_uv() { if [ -z "$_uv_version" ]; then err "uv installation failed." else - say "$_uv_version installed successfully!" + say "📦 $_uv_version installed successfully!" fi } # Install the required Python version setup_python() { - PYTHON_BIN_PATH="$(uv python find "$REQUIRED_PYTHON_VERSION" 2>/dev/null)" || { + PYTHON_BIN_PATH="$(uv python find "$PYTHON_VERSION" 2>/dev/null)" || { PYTHON_BIN_PATH="" } if [ -x "$PYTHON_BIN_PATH" ]; then @@ -237,11 +238,11 @@ setup_python() { show_activity & local _activity_pid=$! - say "Installing Python $REQUIRED_PYTHON_VERSION..." - uv python install "$REQUIRED_PYTHON_VERSION" --preview 2>/dev/null || { + say "Installing Python $PYTHON_VERSION..." + uv python install "$PYTHON_VERSION" --preview 2>/dev/null || { err "Failed to install Python" } - PYTHON_BIN_PATH="$(uv python find "$REQUIRED_PYTHON_VERSION")" || { + PYTHON_BIN_PATH="$(uv python find "$PYTHON_VERSION")" || { err "Failed to find Python" } @@ -251,7 +252,7 @@ setup_python() { if [ -x "$PYTHON_BIN_PATH" ]; then local _python_version="$($PYTHON_BIN_PATH --version 2>&1)" - say "Python $_python_version installed successfully!" + say "🐍 Python $_python_version installed successfully!" else err "Failed to install Python" fi @@ -323,7 +324,7 @@ install_release() { kill $_activity_pid say "" - say "$APP_NAME $VERSION installed successfully!" + say "💥 $APP_NAME $VERSION installed successfully!" } # Install a specific branch/commit/tag from the git repo @@ -359,7 +360,7 @@ install_dev_branch() { kill $_activity_pid say "" - say "$APP_NAME @ $DEV_BRANCH installed successfully!" + say "🔧 $APP_NAME @ $DEV_BRANCH installed successfully!" } # Install the app in the user's site-packages directory and add a executable @@ -496,6 +497,19 @@ parse_args() { VERSION="$2" shift 2 ;; + --python-version=*) + PYTHON_VERSION="${1#*=}" + shift + ;; + --python-version) + if [[ -z "$2" || "$2" == -* ]]; then + err "Error: --python-version requires a value" + usage + exit 1 + fi + PYTHON_VERSION="$2" + shift 2 + ;; --dev-branch=*) DEV_BRANCH="${1#*=}" shift diff --git a/tests/install_script/linux/Dockerfile b/tests/install_script/linux/Dockerfile deleted file mode 100644 index b426d5a5..00000000 --- a/tests/install_script/linux/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM debian:12 - -RUN apt-get update && apt-get upgrade -y - -RUN apt-get install -y \ - build-essential \ - git \ - # \ - #curl - wget - -# build-essential \ - - -WORKDIR /root - -COPY ./install.sh /root -RUN chmod +x /root/install.sh - -#CMD ["bash", "-c", "./install.sh; bash"] -#CMD ["bash", "-c", "./install.sh --verbose; bash"] -#CMD ["bash", "-c", "./install.sh --version=\"0.3.4\"; bash"] -#CMD ["bash", "-c", "./install.sh --dev-branch=main; bash"] -CMD ["bash", "-c", "./install.sh --dev-branch=\"pull/318/head:pr-318\"; bash"] - -# docker build -t debian -f tests/install_script/linux/Dockerfile . && docker run -it debian \ No newline at end of file diff --git a/tests/install_script/run_tests.py b/tests/install_script/run_tests.py new file mode 100644 index 00000000..970c290a --- /dev/null +++ b/tests/install_script/run_tests.py @@ -0,0 +1,109 @@ +import os, sys +import io +import re +import hashlib +import tempfile +from pathlib import Path +import docker +from docker.errors import DockerException + +BASE_DIR = Path(__file__).resolve().parent.parent.parent +PYTHON_VERSIONS: list[str] = [">=3.10,<3.13", "3.10", "3.11", "3.12"] + +# make sure your local Docker install has a public socket +# set credstore: "" in ~/.docker/config.json +client = docker.DockerClient(base_url=f'unix://var/run/docker.sock') + + +def print_green(text: str): + print(f"\033[92m{text}\033[0m") + +def print_red(text: str): + print(f"\033[91m{text}\033[0m") + +def _run_vm(name: str, python_version: str, packages: list[str], command: str) -> str: + dockerfile = f""" +FROM ubuntu:latest +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y {" ".join(packages)} + +WORKDIR /root + +COPY install.sh /root/install.sh +RUN chmod +x /root/install.sh +""" + dockerfile_hash = hashlib.md5(dockerfile.encode("utf-8")).hexdigest() + install_script_hash = hashlib.md5((BASE_DIR / 'install.sh').read_bytes()).hexdigest() + hash = hashlib.md5((dockerfile_hash + install_script_hash).encode("utf-8")).hexdigest() + image_name = F"{re.sub('[<>=,.]', '', python_version)}-{name}-{hash}" + + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir) + script = BASE_DIR / 'install.sh' + with open(path / 'install.sh', 'wb') as f: + f.write(script.read_bytes()) + with open(path / 'Dockerfile', 'w') as f: + f.write(dockerfile) + + image, build_logs = client.images.build( + tag=image_name, + path=tmpdir, + rm=True, + ) + + container = client.containers.run( + image=image, + command=command, + detach=False, + ) + return container.decode("utf-8") + + +def test_default(python_version: str): + result = _run_vm( + test_default.__name__, + python_version, + ["build-essential", "git", "curl"], + "bash -c ./install.sh --python-version={python_version}" + ) + assert "Setup complete!" in result + + +def test_wget(python_version: str): + result = _run_vm( + test_wget.__name__, + python_version, + ["build-essential", "git", "wget"], + "bash -c ./install.sh --python-version={python_version}" + ) + assert "Setup complete!" in result + + +def test_dev_branch(python_version: str): + result = _run_vm( + test_dev_branch.__name__, + python_version, + ["build-essential", "git", "curl"], + "bash -c ./install.sh --dev-branch=main --python-version={python_version}" + ) + assert "Setup complete!" in result + + +if __name__ == "__main__": + if "--quick" in sys.argv: + try: + print(f"{PYTHON_VERSIONS[0]}:test_default", end="\t") + test_default(PYTHON_VERSIONS[0]) + print_green(f"PASS") + except AssertionError: + print_red(f"FAIL") + sys.exit(0) + + for method in [func for func in dir() if func.startswith("test_")]: + for version in PYTHON_VERSIONS: + try: + print(f"{version}:{method}", end="\t") + globals()[method](version) + print_green(f"PASS") + except AssertionError: + print_red(f"FAIL") From 588b127800eac77d307405f2ba1d462c48eae6d5 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Tue, 18 Feb 2025 19:22:48 -0800 Subject: [PATCH 18/22] MacOS fixes. More robust dots. --- install.sh | 81 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 42 insertions(+), 39 deletions(-) mode change 100644 => 100755 install.sh diff --git a/install.sh b/install.sh old mode 100644 new mode 100755 index a28d815b..040ed42c --- a/install.sh +++ b/install.sh @@ -1,12 +1,13 @@ #!/bin/bash +export LANG=en_US.UTF-8 set -e -LOGO=$(cat </dev/null || echo '') local _reset=$(tput sgr0 2>/dev/null || echo '') @@ -179,8 +202,7 @@ install_uv() { else say "Installing uv..." fi - show_activity & - local _activity_pid=$! + show_activity # download with curl or wget local _install_cmd @@ -203,21 +225,13 @@ install_uv() { err "uv installation failed: $_output" fi - # ensure uv is in PATH - if ! check_cmd uv; then - say_verbose "Adding ~/.local/bin to PATH" - update_path "$HOME/.local/bin" - fi - # verify uv installation local _uv_version _uv_version="$(uv --version 2>/dev/null)" || { _uv_version=0 } - kill $_activity_pid - say "" - + end_activity if [ -z "$_uv_version" ]; then err "uv installation failed." else @@ -235,8 +249,7 @@ setup_python() { say "Python $_python_version is available." return 0 else - show_activity & - local _activity_pid=$! + show_activity say "Installing Python $PYTHON_VERSION..." uv python install "$PYTHON_VERSION" --preview 2>/dev/null || { @@ -246,8 +259,7 @@ setup_python() { err "Failed to find Python" } - kill $_activity_pid - say "" + end_activity fi if [ -x "$PYTHON_BIN_PATH" ]; then @@ -261,8 +273,7 @@ setup_python() { # Install an official release of the app install_release() { say "Installing $APP_NAME..." - show_activity & - local _activity_pid=$! + show_activity local _zip_ext if check_cmd tar; then @@ -321,9 +332,7 @@ install_release() { # install & cleanup setup_app "$_dir" rm -rf "$_dir" - - kill $_activity_pid - say "" + end_activity say "💥 $APP_NAME $VERSION installed successfully!" } @@ -335,8 +344,7 @@ install_dev_branch() { fi say "Installing $APP_NAME..." - show_activity & - local _activity_pid=$! + show_activity local _dir="$(ensure mktemp -d)" || return 1 # clone from git @@ -357,9 +365,7 @@ install_dev_branch() { # install & cleanup setup_app "$_dir" rm -rf "$_dir" - - kill $_activity_pid - say "" + end_activity say "🔧 $APP_NAME @ $DEV_BRANCH installed successfully!" } @@ -379,6 +385,7 @@ setup_app() { fi make_python_bin "$HOME/.local/bin/$APP_NAME" + update_path "$HOME/.local/bin" say_verbose "Added bin to ~/.local/bin/$APP_NAME" # verify installation @@ -389,26 +396,22 @@ setup_app() { update_path() { local new_path="$1" - # early exit if path is already present - case ":$PATH:" in - *":$new_path:"*) return 0 ;; - esac - # update for current session export PATH="$new_path:$PATH" + # update for each shell local config_files=( "$HOME/.bashrc" # bash "$HOME/.zshrc" # ssh "$HOME/.profile" # POSIX fallback (sh, ksh, etc.) ) - - # update for each shell for config_file in "${config_files[@]}"; do if [ -f "$config_file" ]; then if ! grep -E "^[^#]*export[[:space:]]+PATH=.*(:$new_path|$new_path:|$new_path\$)" "$config_file" >/dev/null 2>&1; then echo "export PATH=\"$new_path:\$PATH\"" >> "$config_file" - say_verbose "Added $new_path to $config_file" + say_verbose "Added PATH $new_path to $config_file" + else + say_verbose "PATH $new_path already in $config_file" fi fi done @@ -558,13 +561,13 @@ main() { parse_args "$@" say "$LOGO\n" - say "Starting installation..." if check_cmd $APP_NAME; then say "\n$MSG_ALREADY_INSTALLED\n" exit 0 fi + say "Starting installation..." check_dependencies install_uv setup_python @@ -578,4 +581,4 @@ main() { exit 0 } -main "$@" \ No newline at end of file +main "$@" From bd85edb24bf4ad010ff478b1b296651481f27bb5 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Wed, 19 Feb 2025 09:15:03 -0800 Subject: [PATCH 19/22] Uninstall --- install.sh | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index 040ed42c..b7a47d4d 100755 --- a/install.sh +++ b/install.sh @@ -22,6 +22,7 @@ PYTHON_VERSION=">=3.10,<3.13" UV_INSTALLER_URL="https://astral.sh/uv/install.sh" PYTHON_BIN_PATH="" # set after a verified install is found DEV_BRANCH="" # set by --dev-branch flag +DO_UNINSTALL=0 # set by uninstall flag PRINT_VERBOSE=0 PRINT_QUIET=1 @@ -43,6 +44,13 @@ Run 'agentstack update' to update to the latest version. EOF ) +MSG_UNINSTALL=$(cat < Specify version to install (default: $VERSION) --python-version= Specify Python version to install (default: $PYTHON_VERSION) --dev-branch= Install from a specific git branch/commit/tag @@ -114,7 +123,7 @@ err() { say "\n${_red}[ERROR]${_reset}: $1" >&2 say "\nRun with --verbose for more details." say "\nIf you need help, please feel free to open an issue:" - say "\n $REPO_URL/issues\n" + say " $REPO_URL/issues\n" fi exit 1 } @@ -225,10 +234,12 @@ install_uv() { err "uv installation failed: $_output" fi + update_path "$HOME/.local/bin" + # verify uv installation local _uv_version _uv_version="$(uv --version 2>/dev/null)" || { - _uv_version=0 + err "could not find uv" } end_activity @@ -396,8 +407,10 @@ setup_app() { update_path() { local new_path="$1" - # update for current session - export PATH="$new_path:$PATH" + # update for current session if not already set + if ! echo $PATH | grep -q "$new_path"; then + export PATH="$new_path:$PATH" + fi # update for each shell local config_files=( @@ -437,6 +450,34 @@ EOF chmod +x $_program_bin } +uninstall() { + say "Uninstalling $APP_NAME..." + show_activity + + update_path "$HOME/.local/bin" + PYTHON_BIN_PATH="$(uv python find "$PYTHON_VERSION" 2>/dev/null)" || { + PYTHON_BIN_PATH="" + } + + # uninstall the app + local _packages_dir="$($PYTHON_BIN_PATH -m site --user-site 2>/dev/null)" || { + err "Failed to find user site packages directory" + } + say_verbose "Uninstalling from $_packages_dir" + local _uninstall_cmd="uv pip uninstall --python="$PYTHON_BIN_PATH" --target="$_packages_dir" $APP_NAME" + say_verbose "$_uninstall_cmd" + local _uninstall_out="$(eval "$_uninstall_cmd" 2>&1)" + say_verbose "$_uninstall_out" + if [ $? -ne 0 ] || echo "$_uninstall_out" | grep -qi "error\|failed\|exception"; then + err "Failed to uninstall $APP_NAME." + fi + + # remove the bin file + rm -f "$HOME/.local/bin/$APP_NAME" + + end_activity +} + # Download a file. Try curl first, if not installed, use wget instead. download_file() { local _url="$1" @@ -487,6 +528,10 @@ verify_sha256_checksum() { parse_args() { while [[ $# -gt 0 ]]; do case "$1" in + uninstall) + DO_UNINSTALL=1 + shift + ;; --version=*) VERSION="${1#*=}" shift @@ -562,6 +607,12 @@ main() { say "$LOGO\n" + if [ $DO_UNINSTALL -eq 1 ]; then + uninstall + say "\n$MSG_UNINSTALL\n" + exit 0 + fi + if check_cmd $APP_NAME; then say "\n$MSG_ALREADY_INSTALLED\n" exit 0 From 418aa7a12f237ba3167717fb2f694677c0d5d9d6 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Wed, 19 Feb 2025 14:56:52 -0800 Subject: [PATCH 20/22] Insert newline when adding PATH to user profile. --- install.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index b7a47d4d..209938e6 100755 --- a/install.sh +++ b/install.sh @@ -30,8 +30,8 @@ MSG_SUCCESS=$(cat </dev/null 2>&1; then - echo "export PATH=\"$new_path:\$PATH\"" >> "$config_file" + echo -e "\nexport PATH=\"$new_path:\$PATH\"" >> "$config_file" say_verbose "Added PATH $new_path to $config_file" else say_verbose "PATH $new_path already in $config_file" From 56fe0996ccbae173b9ff4000e1a2b40c42f65d63 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Wed, 19 Feb 2025 15:33:45 -0800 Subject: [PATCH 21/22] Remove stale TODO comments. --- install.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/install.sh b/install.sh index 209938e6..e40b817f 100755 --- a/install.sh +++ b/install.sh @@ -73,9 +73,6 @@ OPTIONS: -h, --help Show this help message EOF } -# TODO allow user to specify install path with --target -# TODO allow user to specify Python version with --python-version -# TODO uninstall say() { if [ "1" = "$PRINT_QUIET" ]; then From d627753680844fb5ce0f5befad62d3f21a592eaf Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 20 Feb 2025 13:34:17 -0600 Subject: [PATCH 22/22] Initialize new project via config vars. --- install.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/install.sh b/install.sh index e40b817f..1acb8c7f 100755 --- a/install.sh +++ b/install.sh @@ -23,6 +23,8 @@ UV_INSTALLER_URL="https://astral.sh/uv/install.sh" PYTHON_BIN_PATH="" # set after a verified install is found DEV_BRANCH="" # set by --dev-branch flag DO_UNINSTALL=0 # set by uninstall flag +INIT_TEMPLATE="" +INIT_NAME="" PRINT_VERBOSE=0 PRINT_QUIET=1 @@ -400,6 +402,20 @@ setup_app() { ensure "$APP_NAME" --version > /dev/null } +# Initialize a new user project from a template +init_project() { + if [ -z "$INIT_NAME" ]; then + err "INIT_NAME is not set" + fi + if [ -z "$INIT_TEMPLATE" ]; then + INIT_TEMPLATE='empty' + say_verbose "no template specified, defaulting to 'empty'" + fi + + say "Initializing project '$INIT_NAME' from template '$INIT_TEMPLATE'..." + $APP_NAME init "$INIT_NAME" --template "$INIT_TEMPLATE" +} + # Update PATH in shell config files update_path() { local new_path="$1" @@ -625,6 +641,11 @@ main() { install_release fi + if [ -n "$INIT_NAME" ]; then + init_project + exit 0 + fi + say "\n$MSG_SUCCESS\n" exit 0 }