#!/usr/bin/env bash set -e # oneper - Install Continue VS Code extension from PR builds or latest pre-release # Usage: # oneper install --pr X - Install VSIX from PR number X # oneper install --latest - Install latest pre-release from marketplace REPO="continuedev/continue" ONEPER_DIR="$HOME/.continue/.utils/oneper" show_help() { echo "oneper - Install Continue VS Code extension from builds or latest pre-release" echo "" echo "Usage: oneper [options]" echo " oneper -h | --help" echo " oneper install # Install latest build from main branch (default)" echo "" echo "Commands:" echo " install Download and install VSIX from latest main branch build (default)" echo " install --pr [--platform ]" echo " Download and install VSIX from specified PR number" echo " Platform options: macos, linux (auto-detected by default)" echo " install --latest Install latest pre-release from VS Code marketplace" echo " setup-cron [--interval ] Set up automatic updates (default: 10 minutes)" echo " remove-cron Remove automatic updates" echo " clean Remove all downloaded VSIX files from oneper cache" echo "" echo "Options:" echo " -h, --help Show this help message" echo " --platform Specify platform for builds (macos, linux) --interval Set update interval in minutes (for setup-cron)" echo "" echo "Examples:" echo " oneper install # Install latest build from main branch" echo " oneper install --pr 123 # Install VSIX from PR #123" echo " oneper install --pr 123 --platform linux # Install Linux VSIX from PR #123" echo " oneper install --latest # Install latest pre-release" echo " oneper setup-cron # Auto-update every 10 minutes oneper setup-cron --interval 5 # Auto-update every 5 minutes" echo " oneper remove-cron # Stop auto-updates" echo " oneper clean # Clear cached VSIX files" echo " oneper -h # Show help" echo "" echo "VSIX files are cached in: ~/.continue/.utils/oneper/" } detect_os() { case "$(uname -s)" in "Darwin") echo "macos" ;; "Linux") echo "linux" ;; *) echo "โŒ Unsupported operating system: $(uname -s)" >&2 echo " Supported platforms: macOS, Linux" >&2 exit 1 ;; esac } check_dependencies() { if ! command -v gh &> /dev/null; then echo "โŒ GitHub CLI (gh) is required but not installed." echo " Install with: brew install gh" exit 1 fi if ! command -v code &> /dev/null; then echo "โŒ VS Code CLI (code) is required but not installed." echo " Install VS Code and ensure 'code' command is in PATH" exit 1 fi } ensure_oneper_dir() { if [ ! -d "$ONEPER_DIR" ]; then echo "๐Ÿ“ Creating oneper directory: $ONEPER_DIR" mkdir -p "$ONEPER_DIR" fi } install_from_main() { local platform=${1:-$(detect_os)} # Auto-detect OS if not specified # Map platform names to GitHub runner OS names local artifact_suffix case "$platform" in "macos") artifact_suffix="macOS" ;; "linux") artifact_suffix="Linux" ;; *) echo "โŒ Unknown platform: $platform" echo " Supported platforms: macos, linux" exit 1 ;; esac echo "๐Ÿ” Finding latest main branch build..." # Get the latest successful workflow run for main branch local run_id=$(gh run list \ --repo "$REPO" \ --workflow="main-build.yaml" \ --branch="main" \ --json databaseId,status,conclusion \ --jq ".[] | select(.status == \"completed\" and .conclusion == \"success\") | .databaseId" \ | head -1) if [ -z "$run_id" ]; then echo "โŒ No successful main branch build found" echo " Make sure there are recent commits to main with successful builds" exit 1 fi echo "โœ… Found workflow run: https://github.com/$REPO/actions/runs/$run_id" # Try to get the original PR number from the workflow run local original_pr=$(gh run view "$run_id" --repo "$REPO" --json displayTitle --jq '.displayTitle' | grep -o "pull request #[0-9]*" | grep -o "[0-9]*" || echo "") if [ -n "$original_pr" ]; then echo "๐Ÿ“ฆ This contains artifacts originally from PR #$original_pr" fi echo "๐Ÿ“ฅ Downloading vscode-extension-build-$artifact_suffix artifact..." # Create temporary directory for download local temp_dir=$(mktemp -d) # Download the artifact if ! gh run download "$run_id" \ --repo "$REPO" \ --name "vscode-extension-build-$artifact_suffix" \ --dir "$temp_dir"; then echo "โŒ Failed to download artifact vscode-extension-build-$artifact_suffix" echo " Make sure the workflow run completed successfully for $platform" rm -rf "$temp_dir" exit 1 fi # Find the VSIX file in the downloaded artifact local vsix_file=$(find "$temp_dir" -name "*.vsix" | head -1) if [ -z "$vsix_file" ]; then echo "โŒ No VSIX file found in artifact" rm -rf "$temp_dir" exit 1 fi # Extract version from the original VSIX filename (e.g., continue-1.1.66.vsix -> 1.1.66) local original_filename=$(basename "$vsix_file") local version=$(echo "$original_filename" | sed 's/continue-\(.*\)\.vsix/\1/') if [ -z "$version" ]; then echo "โš ๏ธ Could not extract version from filename: $original_filename" version="unknown" fi # Create new VSIX name with version and "main" suffix local target_path="$ONEPER_DIR/continue-${version}-main.vsix" # Check if file already exists and notify about overwrite if [ -f "$target_path" ]; then echo "โš ๏ธ Overwriting existing VSIX: $target_path" fi echo "๐Ÿ“ฆ Moving VSIX to: $target_path" mv "$vsix_file" "$target_path" # Clean up temp directory rm -rf "$temp_dir" # Install the extension echo "๐Ÿš€ Installing VS Code extension..." if code --install-extension "$target_path" --force; then echo "โœ… Successfully installed Continue extension from main branch" echo "๐Ÿ“„ VSIX file saved to: $target_path" echo "" echo "๐Ÿ’ก To use the new extension version, reload your VS Code window:" echo " โ€ข Press Ctrl+Shift+P (Cmd+Shift+P on Mac)" echo " โ€ข Type 'Developer: Reload Window' and press Enter" echo " โ€ข Or restart VS Code entirely" else echo "โŒ Failed to install extension" exit 1 fi } install_from_pr() { local pr_number=$1 local platform=${2:-$(detect_os)} # Auto-detect OS if not specified if [ -z "$pr_number" ]; then echo "โŒ PR number is required" show_help exit 1 fi # Map platform names to artifact names for the new workflow local artifact_name case "$platform" in "macos") artifact_name="darwin-arm64-vsix" ;; "linux") artifact_name="linux-x64-vsix" ;; *) echo "โŒ Unknown platform: $platform" echo " Supported platforms: macos, linux" exit 1 ;; esac echo "๐Ÿ” Getting branch name for PR #$pr_number..." # First get the branch name for the PR local branch_name=$(gh pr view "$pr_number" --repo "$REPO" --json headRefName --jq '.headRefName') if [ -z "$branch_name" ]; then echo "โŒ Could not find PR #$pr_number" exit 1 fi echo "๐Ÿ” Finding latest workflow run for branch: $branch_name..." # Get the latest successful workflow run for the branch local run_id=$(gh run list \ --repo "$REPO" \ --workflow="pr-build-upload-vsix.yaml" \ --branch="$branch_name" \ --json databaseId,status,conclusion \ --jq ".[] | select(.status == \"completed\" and .conclusion == \"success\") | .databaseId" \ | head -1) if [ -z "$run_id" ]; then echo "โŒ No successful workflow run found for PR #$pr_number" echo " Make sure the PR exists and has a successful CI run" exit 1 fi echo "โœ… Found workflow run: $run_id" echo "๐Ÿ“ฅ Downloading $artifact_name artifact..." # Create temporary directory for download local temp_dir=$(mktemp -d) # Download the artifact if ! gh run download "$run_id" \ --repo "$REPO" \ --name "$artifact_name" \ --dir "$temp_dir"; then echo "โŒ Failed to download artifact $artifact_name" echo " Make sure the workflow run completed successfully for $platform" rm -rf "$temp_dir" exit 1 fi # Find the VSIX file in the downloaded artifact local vsix_file=$(find "$temp_dir" -name "*.vsix" | head -1) if [ -z "$vsix_file" ]; then echo "โŒ No VSIX file found in artifact" rm -rf "$temp_dir" exit 1 fi # Extract full version from the original VSIX filename (e.g., continue-1.1.66-a1b2c3d.vsix -> 1.1.66-a1b2c3d) local original_filename=$(basename "$vsix_file") local full_version=$(echo "$original_filename" | sed 's/continue-\(.*\)\.vsix/\1/') if [ -z "$full_version" ]; then echo "โš ๏ธ Could not extract version from filename: $original_filename" full_version="unknown-pr${pr_number}" fi # Use the full version (which includes commit SHA) for the target filename local target_path="$ONEPER_DIR/continue-${full_version}.vsix" # Check if file already exists and notify about overwrite if [ -f "$target_path" ]; then echo "โš ๏ธ Overwriting existing VSIX: $target_path" fi echo "๐Ÿ“ฆ Moving VSIX to: $target_path" mv "$vsix_file" "$target_path" # Clean up temp directory rm -rf "$temp_dir" # Install the extension echo "๐Ÿš€ Installing VS Code extension..." if code --install-extension "$target_path"; then echo "โœ… Successfully installed Continue extension from PR #$pr_number" echo "๐Ÿ“„ VSIX file saved to: $target_path" echo "๐Ÿ”– Extension version: ${full_version} (includes commit SHA)" echo "" echo "๐Ÿ’ก To use the new extension version, reload your VS Code window:" echo " โ€ข Press Ctrl+Shift+P (Cmd+Shift+P on Mac)" echo " โ€ข Type 'Developer: Reload Window' and press Enter" echo " โ€ข Or restart VS Code entirely" else echo "โŒ Failed to install extension" exit 1 fi } install_latest() { echo "๐Ÿš€ Installing latest pre-release Continue extension..." if code --install-extension --pre-release Continue.continue --force; then echo "โœ… Successfully installed latest pre-release Continue extension" echo "" echo "๐Ÿ’ก To use the new extension version, reload your VS Code window:" echo " โ€ข Press Ctrl+Shift+P (Cmd+Shift+P on Mac)" echo " โ€ข Type 'Developer: Reload Window' and press Enter" echo " โ€ข Or restart VS Code entirely" else echo "โŒ Failed to install extension" exit 1 fi } get_latest_main_run_id() { gh run list \ --repo "$REPO" \ --workflow="main-build.yaml" \ --branch="main" \ --json databaseId,status,conclusion \ --jq ".[] | select(.status == \"completed\" and .conclusion == \"success\") | .databaseId" \ | head -1 } check_and_install_if_new() { local current_run_id=$(get_latest_main_run_id) if [ -z "$current_run_id" ]; then echo "$(date): โŒ No successful main branch build found" >&2 return 1 fi local last_run_file="$ONEPER_DIR/last_installed_run" local last_run_id="" if [ -f "$last_run_file" ]; then last_run_id=$(cat "$last_run_file") fi if [ "$current_run_id" != "$last_run_id" ]; then echo "$(date): ๐Ÿ”„ New build detected (https://github.com/$REPO/runs/$current_run_id), installing..." # Install from main silently if install_from_main > /dev/null 2>&1; then echo "$current_run_id" > "$last_run_file" echo "$(date): โœ… Successfully installed new build from https://github.com/$REPO/actions/runs/$current_run_id" else echo "$(date): โŒ Failed to install new build" >&2 return 1 fi else echo "$(date): ๐Ÿ” No new builds (current: https://github.com/$REPO/actions/runs/$current_run_id)" fi } setup_cron() { local interval=${1:-10} # Default to 10 minutes if not specified echo "๐Ÿ”ง Setting up automatic updates every $interval minutes..." # Get the absolute path to this script local script_path=$(realpath "$0") # Create a wrapper script that calls the check function local cron_script="$ONEPER_DIR/oneper-cron.sh" cat > "$cron_script" << EOF #!/bin/bash # Auto-generated oneper cron script export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:\$PATH" cd "\$(dirname "$script_path")" "$script_path" --cron-check EOF chmod +x "$cron_script" # Create log file local log_file="$ONEPER_DIR/oneper-cron.log" touch "$log_file" # Add to crontab local cron_line="*/$interval * * * * $cron_script >> $log_file 2>&1" # Check if cron job already exists if crontab -l 2>/dev/null | grep -q "oneper-cron.sh"; then echo "โš ๏ธ Oneper cron job already exists. Removing old one..." crontab -l 2>/dev/null | grep -v "oneper-cron.sh" | crontab - fi # Add new cron job (crontab -l 2>/dev/null; echo "$cron_line") | crontab - echo "โœ… Automatic updates configured!" echo "๐Ÿ“Š Logs will be written to: $log_file" echo "๐Ÿ” Check status with: tail -f $log_file" echo "" echo "๐Ÿ’ก To remove automatic updates: oneper remove-cron" } remove_cron() { echo "๐Ÿ—‘๏ธ Removing automatic updates..." # Remove from crontab if crontab -l 2>/dev/null | grep -q "oneper-cron.sh"; then crontab -l 2>/dev/null | grep -v "oneper-cron.sh" | crontab - echo "โœ… Removed cron job" else echo "โ„น๏ธ No oneper cron job found" fi # Clean up files local cron_script="$ONEPER_DIR/oneper-cron.sh" local log_file="$ONEPER_DIR/oneper-cron.log" local last_run_file="$ONEPER_DIR/last_installed_run" [ -f "$cron_script" ] && rm "$cron_script" && echo "๐Ÿ—‘๏ธ Removed cron script" [ -f "$log_file" ] && rm "$log_file" && echo "๐Ÿ—‘๏ธ Removed log file" [ -f "$last_run_file" ] && rm "$last_run_file" && echo "๐Ÿ—‘๏ธ Removed state file" echo "โœ… Automatic updates removed successfully" } clean_cache() { if [ ! -d "$ONEPER_DIR" ]; then echo "๐Ÿ“ Oneper cache directory doesn't exist: $ONEPER_DIR" return 0 fi local file_count=$(find "$ONEPER_DIR" -name "*.vsix" | wc -l | tr -d ' ') if [ "$file_count" -eq 0 ]; then echo "โœจ Oneper cache is already empty" return 0 fi echo "๐Ÿงน Removing $file_count VSIX file(s) from oneper cache..." rm -f "$ONEPER_DIR"/*.vsix echo "โœ… Oneper cache cleared successfully" echo "๐Ÿ“ Cache directory: $ONEPER_DIR" } main() { # Handle special internal commands first case "$1" in "--cron-check") # Internal command used by cron job ensure_oneper_dir check_and_install_if_new exit $? ;; "-h"|"--help") show_help exit 0 ;; esac # If no arguments, default to installing from main if [ $# -eq 0 ]; then check_dependencies ensure_oneper_dir install_from_main exit 0 fi check_dependencies ensure_oneper_dir case "$1" in "install") case "$2" in "--pr") # Parse PR number and optional platform local pr_number="$3" local platform=$(detect_os) # auto-detect OS # Check if --platform is specified to override auto-detection if [ "$4" = "--platform" ] && [ -n "$5" ]; then platform="$5" fi install_from_pr "$pr_number" "$platform" ;; "--latest") install_latest ;; "--platform") # Handle: oneper install --platform (install from main with specific platform) local platform="$3" install_from_main "$platform" ;; "") # Handle: oneper install (install from main with auto-detected platform) install_from_main ;; *) echo "โŒ Unknown install option: $2" show_help exit 1 ;; esac ;; "setup-cron") # Check for --interval option local interval=10 if [ "$2" = "--interval" ] && [ -n "$3" ]; then interval="$3" # Validate interval is a positive integer if ! [[ "$interval" =~ ^[1-9][0-9]*$ ]]; then echo "โŒ Invalid interval: $interval. Must be a positive integer." exit 1 fi fi setup_cron "$interval" ;; "remove-cron") remove_cron ;; "clean") clean_cache ;; *) echo "โŒ Unknown command: $1" show_help exit 1 ;; esac } main "$@"