|
#!/bin/bash |
|
|
|
# Enable strict error handling |
|
set -euo pipefail |
|
IFS=$'\n\t' |
|
|
|
# Export PS4 for debugging (optional) |
|
#export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }' |
|
# Uncomment the next line to enable debugging |
|
#set -x |
|
|
|
# Default configurations |
|
INPUT_FILES="files.txt" |
|
PROMPT_FILE="prompt.txt" |
|
CONVENTIONS_FILE="CONVENTIONS.md" |
|
SENSITIVE_FILES="sensitive_files.txt" |
|
TEST_CMD="./start-script.sh" |
|
MAX_CHAT_HISTORY_TOKENS=2500 |
|
EDIT_FORMAT="whole" |
|
EDITOR_EDIT_FORMAT="full" |
|
LOG_FILE="aider_setup.log" |
|
DRY_RUN=false |
|
# python 3.9 or above is required |
|
REQUIRED_PYTHON_VERSION="3.9" |
|
|
|
# Load configuration if the file exists |
|
CONFIG_FILE="${HOME}/.aider_config" |
|
if [[ -f "$CONFIG_FILE" ]]; then |
|
source "$CONFIG_FILE" |
|
fi |
|
|
|
# Initialize variables |
|
gui_mode=false |
|
use_vault=false |
|
model="" |
|
input_files=${INPUT_FILES} |
|
prompt_file=${PROMPT_FILE} |
|
test_cmd=${TEST_CMD} # Ensure test_cmd is initialized |
|
max_chat_history_tokens=${MAX_CHAT_HISTORY_TOKENS} |
|
edit_format=${EDIT_FORMAT} |
|
editor_edit_format=${EDITOR_EDIT_FORMAT} |
|
log_file=${LOG_FILE} |
|
dry_run=${DRY_RUN} |
|
|
|
|
|
# Function to log messages |
|
log() { |
|
echo "$(date '+%Y-%m-%d %H:%M:%S') : $*" | tee -a "$LOG_FILE" |
|
} |
|
|
|
# Function to display help |
|
display_help() { |
|
cat << EOF |
|
Usage: $(basename "$0") [options] |
|
|
|
Options: |
|
--install [model] Install specific aider model |
|
--create-venv Create and activate a virtual environment and install aider module |
|
--list-models [category] List all available models for a category |
|
--list-categories List all available model categories |
|
--use-model <model> Start aider with a specific model |
|
--set-api-key <category> <key> Set the API key for a specific category |
|
--use-vault Use Vault to fetch API keys |
|
--dry-run Simulate actions without making any changes |
|
--gui Run aider in your browser |
|
--input-files <file> Specify input files (default: files.txt) |
|
--prompt-file <file> Specify prompt file (default: prompt.txt) |
|
--test-cmd <command> Specify test command (default: ./micro-agent-start.sh) |
|
--max-chat-history-tokens <num> Set max chat history tokens (default: 2500) |
|
--edit-format <format> Set edit format (default: whole) |
|
--editor-edit-format <format> Set editor edit format (default: full) |
|
--help Display this help message |
|
--cleanup-venv Remove the virtual environment |
|
--conventions-file <file> Specify conventions file (default: CONVENTIONS.md) |
|
--sensitive-files <file> Specify sensitive files (default: sensitive_files.txt) |
|
|
|
For more information, visit: https://aider.chat/ |
|
EOF |
|
} |
|
|
|
# Function to check Python version |
|
check_python_version() { |
|
local python_version |
|
python_version=$(python3 -c 'import sys; print(".".join(map(str, sys.version_info[:2])))') |
|
|
|
if [[ $(printf '%s\n' "$python_version" "$REQUIRED_PYTHON_VERSION" | sort -V | head -n1) != "$REQUIRED_PYTHON_VERSION" ]]; then |
|
log "Error: Python $REQUIRED_PYTHON_VERSION or higher is required. Current version: $python_version." |
|
exit 1 |
|
fi |
|
} |
|
|
|
# Function to create and activate virtual environment |
|
create_venv() { |
|
check_python_version |
|
log "Creating virtual environment..." |
|
python3 -m venv aider-env |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: Failed to create virtual environment." |
|
exit 1 |
|
fi |
|
|
|
log "Activating virtual environment..." |
|
source aider-env/bin/activate |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: Failed to activate virtual environment." |
|
exit 1 |
|
fi |
|
|
|
log "Installing necessary Python packages..." |
|
pip install aider-chat questionary inquirer shell-gpt |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: Failed to install Python packages." |
|
exit 1 |
|
fi |
|
|
|
update_gitignore |
|
} |
|
|
|
# Function to activate virtual environment if it exists |
|
activate_venv() { |
|
if [[ -d "aider-env" ]]; then |
|
log "Activating existing virtual environment..." |
|
source aider-env/bin/activate |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: Failed to activate virtual environment." |
|
exit 1 |
|
fi |
|
else |
|
log "Virtual environment not found. Creating one..." |
|
create_venv |
|
fi |
|
} |
|
|
|
# Function to update .gitignore |
|
update_gitignore() { |
|
local gitignore_file=".gitignore" |
|
if [[ ! -f "$gitignore_file" ]]; then |
|
touch "$gitignore_file" |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: Failed to create .gitignore file." |
|
exit 1 |
|
fi |
|
fi |
|
|
|
local entries=("aider-env/" "aider-env/bin/activate" "aider_setup.sh") |
|
for entry in "${entries[@]}"; do |
|
grep -qxF "$entry" "$gitignore_file" || echo "$entry" >> "$gitignore_file" |
|
done |
|
log "Updated .gitignore." |
|
} |
|
|
|
# Function to list all available categories |
|
list_categories() { |
|
log "Listing available categories:" |
|
echo "openai" |
|
echo "anthropic" |
|
echo "gemini" |
|
echo "groq" |
|
echo "azure" |
|
echo "cohere" |
|
echo "deepseek" |
|
echo "ollama" |
|
echo "openrouter" |
|
} |
|
|
|
# Function to list available models for a category |
|
list_models() { |
|
local category=$1 |
|
log "Listing available models for category: $category" |
|
aider --models "$category" |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: Failed to list models for category: $category." |
|
exit 1 |
|
fi |
|
} |
|
|
|
# Function to set API keys (local configuration) |
|
set_api_key() { |
|
local category=$1 |
|
local key_value=$2 |
|
local key_name="" |
|
|
|
case $category in |
|
openai) key_name="OPENAI_API_KEY" ;; |
|
anthropic) key_name="ANTHROPIC_API_KEY" ;; |
|
gemini) key_name="GEMINI_API_KEY" ;; |
|
groq) key_name="GROQ_API_KEY" ;; |
|
azure) key_name="AZURE_API_KEY" ;; |
|
cohere) key_name="COHERE_API_KEY" ;; |
|
deepseek) key_name="DEEPSEEK_API_KEY" ;; |
|
ollama) key_name="OLLAMA_API_KEY" ;; |
|
openrouter) key_name="OPENROUTER_API_KEY" ;; |
|
*) log "Error: Unknown category: $category." ; exit 1 ;; |
|
esac |
|
|
|
local model_keys_file=~/model_keys.conf |
|
|
|
# Ensure the file exists |
|
touch "$model_keys_file" |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: Failed to create or access $model_keys_file." |
|
exit 1 |
|
fi |
|
|
|
# Check if the category already exists and update it |
|
if grep -q "^$category:" "$model_keys_file"; then |
|
sed -i "s|^$category:.*|$category:$key_name:$key_value|" "$model_keys_file" |
|
else |
|
echo "$category:$key_name:$key_value" >> "$model_keys_file" |
|
fi |
|
|
|
# Secure the keys file |
|
chmod 600 "$model_keys_file" |
|
|
|
log "API key for $category set successfully." |
|
} |
|
|
|
# Function to login to HCP and initialize Vault secrets |
|
hcp_login() { |
|
log "Logging into HCP..." |
|
hcp auth login |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: Failed to login to HCP." |
|
exit 1 |
|
fi |
|
|
|
log "Initializing Vault secrets..." |
|
hcp profile init --vault-secrets |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: Failed to initialize Vault secrets." |
|
exit 1 |
|
fi |
|
} |
|
|
|
# Function to read secret from HCP Vault |
|
load_api_keys_from_hcp_vault() { |
|
local category=$1 |
|
local secret_path="" |
|
case $category in |
|
openai) secret_path="openai" ;; |
|
anthropic) secret_path="secret/data/anthropic/anthropic_api_key" ;; |
|
gemini) secret_path="gemini" ;; |
|
groq) secret_path="secret/data/groq/groq_api_key" ;; |
|
azure) secret_path="secret/data/azure/azure_api_key" ;; |
|
cohere) secret_path="secret/data/cohere/cohere_api_key" ;; |
|
deepseek) secret_path="deepseek" ;; |
|
ollama) secret_path="secret/data/ollama/ollama_api_key" ;; |
|
openrouter) secret_path="secret/data/openrouter/openrouter_api_key" ;; |
|
*) log "Error: Unknown category: $category." ; exit 1 ;; |
|
esac |
|
|
|
log "Fetching secret for $category from HCP Vault at path '$secret_path'..." |
|
local api_key |
|
api_key=$(hcp vault-secrets secrets open "$secret_path" --format=json | jq -r '.static_version.value') |
|
|
|
if [[ -z "$api_key" ]]; then |
|
log "Error: API key for $category not found in HCP Vault at path '$secret_path'." |
|
exit 1 |
|
fi |
|
|
|
local key_name |
|
key_name=$(echo "${category}_API_KEY" | tr '[:lower:]' '[:upper:]') |
|
export "$key_name"="$api_key" |
|
log "Using $key_name." |
|
} |
|
|
|
# Function to load API keys from local configuration |
|
load_api_keys() { |
|
local model_keys_file=~/model_keys.conf |
|
if [[ ! -f "$model_keys_file" ]]; then |
|
log "Error: Local API keys file not found at $model_keys_file." |
|
return 1 |
|
fi |
|
|
|
while IFS=: read -r category key_name key_value; do |
|
export "$key_name"="$key_value" |
|
done < "$model_keys_file" |
|
|
|
log "Loaded API keys from $model_keys_file." |
|
} |
|
|
|
# Function to validate file existence |
|
validate_file() { |
|
local file_path=$1 |
|
local description=$2 |
|
if [[ -n "$file_path" && ! -f "$file_path" ]]; then |
|
log "Error: $description file '$file_path' does not exist." |
|
exit 1 |
|
fi |
|
} |
|
|
|
# Function to use a specific model |
|
use_model() { |
|
local model=$1 |
|
local gui_mode=$2 |
|
local use_vault=$3 |
|
|
|
# Extract category from model name (assuming format category/model) |
|
local model_category="${model%%/*}" |
|
if [[ -z "$model_category" ]]; then |
|
log "Error: Invalid model format '$model'. Expected 'category/model'." |
|
exit 1 |
|
fi |
|
|
|
if [[ $use_vault == true ]]; then |
|
# Check dependencies |
|
for cmd in hcp jq; do |
|
if ! command -v "$cmd" &> /dev/null; then |
|
log "Error: Required command '$cmd' is not installed. Please install it to proceed." |
|
exit 1 |
|
fi |
|
done |
|
|
|
# HCP Login and Vault initialization |
|
hcp_login |
|
# Load API keys from HCP Vault |
|
load_api_keys_from_hcp_vault "$model_category" |
|
else |
|
# Load API keys from local configuration |
|
load_api_keys || { |
|
log "Error: Failed to load local API keys." |
|
exit 1 |
|
} |
|
fi |
|
|
|
local key_name |
|
key_name=$(echo "${model_category}_API_KEY" | tr '[:lower:]' '[:upper:]') |
|
|
|
# Verify if the API key is set |
|
if [[ -z ${!key_name} ]]; then |
|
log "Error: Missing API key for $model_category: $key_name." |
|
log "Please set the API key using HCP Vault or local configuration." |
|
exit 1 |
|
fi |
|
|
|
if [[ $gui_mode == true ]]; then |
|
log "Starting aider in GUI mode with model: $model." |
|
aider --gui --model "$model" |
|
else |
|
log "Starting aider with model: $model." |
|
|
|
# Validate required files |
|
validate_file "$input_files" "Input files" |
|
validate_file "$prompt_file" "Prompt" |
|
validate_file "$CONVENTIONS_FILE" "Conventions" |
|
validate_file "$SENSITIVE_FILES" "Sensitive files" # Correct validation for sensitive files |
|
|
|
# Validate test command if provided |
|
if [[ -n "$test_cmd" ]]; then |
|
if [[ ! -x "$test_cmd" ]]; then |
|
log "Error: Test command '$test_cmd' is not executable." |
|
exit 1 |
|
fi |
|
if [[ ! -f "$test_cmd" ]]; then |
|
log "Error: Test command file '$test_cmd' does not exist." |
|
log "Please create the file with the necessary commands to start the program or run test cases." |
|
exit 1 |
|
fi |
|
fi |
|
|
|
# Construct the aider command using an array for safety |
|
local aider_cmd=(aider) |
|
|
|
# source env |
|
source aider-env/bin/activate |
|
|
|
# Append mandatory options |
|
aider_cmd+=( $(cat $input_files) |
|
--architect |
|
--model "$model" |
|
--editor-model "$model" |
|
--auto-commits |
|
--max-chat-history-tokens "$max_chat_history_tokens" |
|
--auto-test |
|
--suggest-shell-commands |
|
--check-update |
|
--edit-format "$edit_format" |
|
--editor-edit-format "$editor_edit_format" |
|
--read "$CONVENTIONS_FILE" |
|
--read "$SENSITIVE_FILES" |
|
--yes |
|
--cache-prompts |
|
--show-diffs |
|
--subtree-only) |
|
|
|
# Conditionally append optional options |
|
[[ -n "$prompt_file" ]] && aider_cmd+=(--message-file "$prompt_file") |
|
[[ -n "$test_cmd" ]] && aider_cmd+=(--test-cmd "$test_cmd") |
|
|
|
# Optional: Implement dry-run functionality |
|
if [[ $DRY_RUN == true ]]; then |
|
log "Dry run: The following command would be executed:" |
|
printf '%q ' "${aider_cmd[@]}" |
|
echo |
|
else |
|
# Execute the command |
|
"${aider_cmd[@]}" |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: 'aider' command failed." |
|
exit 1 |
|
fi |
|
fi |
|
fi |
|
} |
|
|
|
# Function to remove virtual environment |
|
cleanup_venv() { |
|
if [[ -d "aider-env" ]]; then |
|
log "Removing virtual environment..." |
|
rm -rf aider-env |
|
if [[ $? -ne 0 ]]; then |
|
log "Error: Failed to remove virtual environment." |
|
exit 1 |
|
fi |
|
log "Virtual environment removed successfully." |
|
else |
|
log "No virtual environment found to remove." |
|
fi |
|
} |
|
|
|
# Parse arguments |
|
while [[ $# -gt 0 ]]; do |
|
case $1 in |
|
--create-venv) |
|
create_venv |
|
shift |
|
;; |
|
--list-categories) |
|
list_categories |
|
shift |
|
;; |
|
--list-models) |
|
activate_venv |
|
if [[ -z "${2:-}" ]]; then |
|
log "Error: Missing category for --list-models." |
|
display_help |
|
exit 1 |
|
fi |
|
list_models "$2" |
|
shift 2 |
|
;; |
|
--set-api-key) |
|
if [[ -z "${2:-}" || -z "${3:-}" ]]; then |
|
log "Error: Missing arguments for --set-api-key." |
|
display_help |
|
exit 1 |
|
fi |
|
set_api_key "$2" "$3" |
|
shift 3 |
|
;; |
|
--use-model) |
|
if [[ -z "${2:-}" ]]; then |
|
log "Error: Missing model for --use-model." |
|
display_help |
|
exit 1 |
|
fi |
|
model=$2 |
|
shift 2 |
|
;; |
|
--use-vault) |
|
use_vault=true |
|
shift |
|
;; |
|
--gui) |
|
gui_mode=true |
|
shift |
|
;; |
|
--input-files) |
|
if [[ -z "${2:-}" ]]; then |
|
log "Error: Missing file for --input-files." |
|
display_help |
|
exit 1 |
|
fi |
|
input_files=$2 |
|
shift 2 |
|
;; |
|
--prompt-file) |
|
if [[ -z "${2:-}" ]]; then |
|
log "Error: Missing file for --prompt-file." |
|
display_help |
|
exit 1 |
|
fi |
|
prompt_file=$2 |
|
shift 2 |
|
;; |
|
--test-cmd) |
|
if [[ -z "${2:-}" ]]; then |
|
log "Error: Missing command for --test-cmd." |
|
display_help |
|
exit 1 |
|
fi |
|
test_cmd=$2 |
|
shift 2 |
|
;; |
|
--max-chat-history-tokens) |
|
if [[ -z "${2:-}" ]]; then |
|
log "Error: Missing number for --max-chat-history-tokens." |
|
display_help |
|
exit 1 |
|
fi |
|
max_chat_history_tokens=$2 |
|
shift 2 |
|
;; |
|
--edit-format) |
|
if [[ -z "${2:-}" ]]; then |
|
log "Error: Missing format for --edit-format." |
|
display_help |
|
exit 1 |
|
fi |
|
edit_format=$2 |
|
shift 2 |
|
;; |
|
--editor-edit-format) |
|
if [[ -z "${2:-}" ]]; then |
|
log "Error: Missing format for --editor-edit-format." |
|
display_help |
|
exit 1 |
|
fi |
|
editor_edit_format=$2 |
|
shift 2 |
|
;; |
|
--dry-run) |
|
DRY_RUN=true |
|
shift |
|
;; |
|
--help) |
|
display_help |
|
exit 0 |
|
;; |
|
--cleanup-venv) |
|
cleanup_venv |
|
exit 0 |
|
;; |
|
--conventions-file) |
|
if [[ -z "${2:-}" ]]; then |
|
log "Error: Missing file for --conventions-file." |
|
display_help |
|
exit 1 |
|
fi |
|
CONVENTIONS_FILE=$2 |
|
shift 2 |
|
;; |
|
--sensitive-files) |
|
if [[ -z "${2:-}" ]]; then |
|
log "Error: Missing file for --sensitive-files." |
|
display_help |
|
exit 1 |
|
fi |
|
SENSITIVE_FILES=$2 |
|
shift 2 |
|
;; |
|
*) |
|
log "Error: Invalid option: $1. Use --help for usage information." |
|
display_help |
|
exit 1 |
|
;; |
|
esac |
|
done |
|
|
|
# Ensure .gitignore is updated when script is run |
|
update_gitignore |
|
|
|
# Run aider with specified model and mode |
|
if [[ -n "$model" ]]; then |
|
use_model "$model" "$gui_mode" "$use_vault" |
|
else |
|
log "Error: No model specified. Use --use-model <model> to specify a model." |
|
display_help |
|
exit 1 |
|
fi |
|
|