diff --git a/scripts/suggest-optimize.sh b/scripts/suggest-optimize.sh index fa12c15..eafb5b9 100755 --- a/scripts/suggest-optimize.sh +++ b/scripts/suggest-optimize.sh @@ -9,48 +9,8 @@ LOGFILE="/tmp/codeflash-hook-debug.log" exec 2>>"$LOGFILE" set -x -# Read stdin (Stop hook pipes context as JSON via stdin) -INPUT=$(cat) +# ---- Helper functions (above BASH_SOURCE guard for testability) ---- -# If the stop hook is already active (Claude already responded to a previous block), -# allow the stop to proceed to avoid an infinite block loop. -STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false") -if [ "$STOP_HOOK_ACTIVE" = "true" ]; then - exit 0 -fi - -## Per-project tracker keyed on repo root (resolve symlinks so PWD and REPO_ROOT share a prefix) -REPO_ROOT=$(cd "$(git rev-parse --show-toplevel 2>/dev/null)" && pwd -P) || exit 0 -cd "$(pwd -P)" - -# --- Check if codeflash is already auto-allowed in .claude/settings.json --- -CODEFLASH_AUTO_ALLOWED="false" -SETTINGS_JSON="$REPO_ROOT/.claude/settings.json" -if [ -f "$SETTINGS_JSON" ]; then - if jq -e '.permissions.allow // [] | any(test("codeflash"))' "$SETTINGS_JSON" >/dev/null 2>&1; then - CODEFLASH_AUTO_ALLOWED="true" - fi -fi - -# --- Detect new commits with Python/Java/JS/TS files since session started --- - -# Extract transcript_path from hook input to determine session start time -TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null || true) -if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then - exit 0 -fi -TRANSCRIPT_DIR=$(dirname "$TRANSCRIPT_PATH") - -# --- Cheap gate: skip if HEAD hasn't changed since last check --- -CURRENT_HEAD=$(git rev-parse HEAD 2>/dev/null) || exit 0 -LAST_HEAD_FILE="$TRANSCRIPT_DIR/codeflash-last-head" -if [ -f "$LAST_HEAD_FILE" ] && [ "$(cat "$LAST_HEAD_FILE")" = "$CURRENT_HEAD" ]; then - exit 0 -fi -echo "$CURRENT_HEAD" > "$LAST_HEAD_FILE" - -# Get the transcript file's creation (birth) time as the session start timestamp. -# This predates any commits Claude could have made in this session. get_file_birth_time() { local file="$1" if [[ "$(uname)" == "Darwin" ]]; then @@ -66,104 +26,39 @@ get_file_birth_time() { fi } -SESSION_START=$(get_file_birth_time "$TRANSCRIPT_PATH") -if [ -z "$SESSION_START" ] || [ "$SESSION_START" = "0" ]; then - exit 0 -fi - -# Find commits with Python/Java/JS/TS files made after the session started -CHANGED_COMMITS=$(git log --after="@$SESSION_START" --name-only --diff-filter=ACMR --pretty=format: -- '*.py' '*.java' '*.js' '*.ts' '*.jsx' '*.tsx' 2>/dev/null | sort -u | grep -v '^$' || true) -if [ -z "$CHANGED_COMMITS" ]; then - exit 0 -fi - -# Determine which language families actually had changes -HAS_PYTHON_CHANGES="false" -HAS_JS_CHANGES="false" -if echo "$CHANGED_COMMITS" | grep -qE '\.py$'; then - HAS_PYTHON_CHANGES="true" -fi -if echo "$CHANGED_COMMITS" | grep -qE '\.(js|ts|jsx|tsx)$'; then - HAS_JS_CHANGES="true" -fi - -# Dedup: don't trigger twice for the same set of changes across sessions. -SEEN_MARKER="$TRANSCRIPT_DIR/codeflash-seen" - -COMMIT_HASH=$(git log --after="@$SESSION_START" --pretty=format:%H -- '*.py' '*.java' '*.js' '*.ts' '*.jsx' '*.tsx' 2>/dev/null | shasum -a 256 | cut -d' ' -f1) -if [ -f "$SEEN_MARKER" ] && grep -qF "$COMMIT_HASH" "$SEEN_MARKER" 2>/dev/null; then - exit 0 -fi -echo "$COMMIT_HASH" >> "$SEEN_MARKER" - -# --- From here on, we know there are new commits to optimize --- - -# --- Check if CODEFLASH_API_KEY is available --- -OAUTH_SCRIPT="$(dirname "$0")/oauth-login.sh" - -has_api_key() { - # Check env var - if [ -n "${CODEFLASH_API_KEY:-}" ] && [[ "${CODEFLASH_API_KEY}" == cf-* ]]; then - return 0 - fi - # Check Unix shell RC files - for rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile" "$HOME/.kshrc" "$HOME/.cshrc"; do - if [ -f "$rc" ] && grep -q '^export CODEFLASH_API_KEY="cf-' "$rc" 2>/dev/null; then - return 0 - fi - done - # Check Windows-specific files (PowerShell / CMD, matching codeflash CLI) - for rc in "$HOME/codeflash_env.ps1" "$HOME/codeflash_env.bat"; do - if [ -f "$rc" ] && grep -q 'CODEFLASH_API_KEY.*cf-' "$rc" 2>/dev/null; then - return 0 - fi - done - return 1 -} - -LOGIN_STEP="" -if ! has_api_key; then - LOGIN_STEP=" -- Run \`${OAUTH_SCRIPT}\` to log in to Codeflash. If it exits with code 0, the key is saved. If it exits with code 2 (headless environment), parse the JSON output for the \`url\` and \`state_file\`, ask the user to visit the URL and provide the authorization code, then run \`${OAUTH_SCRIPT} --exchange-code \` to complete the login." -fi - -# Walk from $PWD upward to $REPO_ROOT looking for project config. -# Sets: PROJECT_TYPE, PROJECT_DIR, PROJECT_CONFIG_PATH, PROJECT_CONFIGURED -detect_project() { - PROJECT_TYPE="" - PROJECT_DIR="" - PROJECT_CONFIG_PATH="" +# Walk from $PWD upward to $REPO_ROOT checking ALL config types at each level. +# Sets: PROJECT_CONFIGURED, FOUND_CONFIGS (space-separated), PROJECT_DIR +detect_any_config() { PROJECT_CONFIGURED="false" + FOUND_CONFIGS="" + PROJECT_DIR="" local search_dir="$PWD" while true; do - # Check codeflash.toml first (Java projects) + # Check codeflash.toml (Java projects) if [ -f "$search_dir/codeflash.toml" ]; then - PROJECT_TYPE="java" - PROJECT_DIR="$search_dir" - PROJECT_CONFIG_PATH="$search_dir/codeflash.toml" if grep -q '\[tool\.codeflash\]' "$search_dir/codeflash.toml" 2>/dev/null; then PROJECT_CONFIGURED="true" + FOUND_CONFIGS="${FOUND_CONFIGS:+$FOUND_CONFIGS }codeflash.toml" + [ -z "$PROJECT_DIR" ] && PROJECT_DIR="$search_dir" fi - break fi + # Check pyproject.toml (Python projects) if [ -f "$search_dir/pyproject.toml" ]; then - PROJECT_TYPE="python" - PROJECT_DIR="$search_dir" - PROJECT_CONFIG_PATH="$search_dir/pyproject.toml" if grep -q '\[tool\.codeflash\]' "$search_dir/pyproject.toml" 2>/dev/null; then PROJECT_CONFIGURED="true" + FOUND_CONFIGS="${FOUND_CONFIGS:+$FOUND_CONFIGS }pyproject.toml" + [ -z "$PROJECT_DIR" ] && PROJECT_DIR="$search_dir" fi - break fi + # Check package.json (JS/TS projects) if [ -f "$search_dir/package.json" ]; then - PROJECT_TYPE="js" - PROJECT_DIR="$search_dir" - PROJECT_CONFIG_PATH="$search_dir/package.json" if jq -e '.codeflash' "$search_dir/package.json" >/dev/null 2>&1; then PROJECT_CONFIGURED="true" + FOUND_CONFIGS="${FOUND_CONFIGS:+$FOUND_CONFIGS }package.json" + [ -z "$PROJECT_DIR" ] && PROJECT_DIR="$search_dir" fi - break fi + # Move to parent directory if [ "$search_dir" = "$REPO_ROOT" ]; then break fi @@ -179,167 +74,202 @@ detect_project() { done } -# Discover project config -detect_project - -CHECK_DIR="${PROJECT_DIR:-$PWD}" - -# --- JS/TS project path --------------------------------------------------- -if [ "$PROJECT_TYPE" = "js" ] && [ "$HAS_JS_CHANGES" = "true" ]; then - INSTALL_CMD="npm install --save-dev codeflash" - - # Check if codeflash npm package is available +# Unified binary resolution: venv -> PATH -> uv run -> npx +# Sets: CODEFLASH_BIN, CODEFLASH_INSTALLED +find_codeflash_binary() { + CODEFLASH_BIN="" CODEFLASH_INSTALLED="false" - if (cd "$CHECK_DIR" && npx codeflash --version >/dev/null 2>&1); then + # a. Active venv + if [ -n "${VIRTUAL_ENV:-}" ] && [ -x "${VIRTUAL_ENV}/bin/codeflash" ]; then + CODEFLASH_BIN="${VIRTUAL_ENV}/bin/codeflash" CODEFLASH_INSTALLED="true" + return fi + # b. PATH + if command -v codeflash >/dev/null 2>&1; then + CODEFLASH_BIN="codeflash" + CODEFLASH_INSTALLED="true" + return + fi + # c. uv run + if uv run codeflash --version >/dev/null 2>&1; then + CODEFLASH_BIN="uv run codeflash" + CODEFLASH_INSTALLED="true" + return + fi + # d. npx + if npx codeflash --version >/dev/null 2>&1; then + CODEFLASH_BIN="npx codeflash" + CODEFLASH_INSTALLED="true" + return + fi +} - # Not configured → prompt for config (+ install if needed) - if [ "$PROJECT_CONFIGURED" != "true" ]; then - if [ -n "$PROJECT_CONFIG_PATH" ]; then - TARGET_MSG="Add a \`\"codeflash\"\` key to \`$PROJECT_CONFIG_PATH\`" - else - TARGET_MSG="Create \`$REPO_ROOT/package.json\` (via \`npm init -y\`) and add a \`\"codeflash\"\` key" - fi +# Parse changed files to detect which languages have changes. +# Sets: CHANGED_LANGS (space-separated: python java javascript) +detect_changed_languages() { + CHANGED_LANGS="" + if echo "$CHANGED_COMMITS" | grep -q '\.py$'; then + CHANGED_LANGS="python" + fi + if echo "$CHANGED_COMMITS" | grep -q '\.java$'; then + CHANGED_LANGS="${CHANGED_LANGS:+$CHANGED_LANGS }java" + fi + if echo "$CHANGED_COMMITS" | grep -qE '\.(js|ts|jsx|tsx)$'; then + CHANGED_LANGS="${CHANGED_LANGS:+$CHANGED_LANGS }javascript" + fi +} - INSTALL_STEP="" - if [ "$CODEFLASH_INSTALLED" != "true" ]; then - INSTALL_STEP=" -- First, install codeflash by running \`${INSTALL_CMD}\` in \`${CHECK_DIR}\`." +# Check if CODEFLASH_API_KEY is available in env or shell RC files +has_api_key() { + # Check env var + if [ -n "${CODEFLASH_API_KEY:-}" ] && [[ "${CODEFLASH_API_KEY}" == cf-* ]]; then + return 0 + fi + # Check Unix shell RC files + for rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.profile" "$HOME/.kshrc" "$HOME/.cshrc"; do + if [ -f "$rc" ] && grep -q '^export CODEFLASH_API_KEY="cf-' "$rc" 2>/dev/null; then + return 0 fi - - SETUP_MSG="JS/TS files were changed in a recent commit, but codeflash is not configured yet. - -Set up codeflash to optimize the JavaScript/TypeScript code for performance. Automatically determine the following by exploring the project's actual directory structure using Glob and Read (do NOT present options to the user — discover the correct paths yourself): -1. The relative path with respect to package.json to the root of the JS/TS module — typically \".\" for the root directory or \"src\". -2. The relative path with respect to package.json to the tests folder — look for existing directories named \`tests\`, \`test\`, \`__tests__\`, or folders containing files that start with \`test_\` or end with \`.test.js\`/\`.spec.ts\`. If none exist, default to \`tests\`. - -Once determined:${INSTALL_STEP} -- If the tests folder does not exist, create it with mkdir -p. -- ${TARGET_MSG}. Read the existing package.json, parse it, add/update the \"codeflash\" key with the discovered values, and write back with 2-space indentation: - -{ - \"codeflash\": { - \"moduleRoot\": \"\", - \"testsRoot\": \"\", - \"formatterCmds\": [\"disabled\"], - \"ignorePaths\": [\"dist\", \"**/node_modules\", \"**/__tests__\"] - } + done + # Check Windows-specific files (PowerShell / CMD, matching codeflash CLI) + for rc in "$HOME/codeflash_env.ps1" "$HOME/codeflash_env.bat"; do + if [ -f "$rc" ] && grep -q 'CODEFLASH_API_KEY.*cf-' "$rc" 2>/dev/null; then + return 0 + fi + done + return 1 } -Once the configuration is written, run \`npx codeflash --subagent\` as a background Bash task (run_in_background: true)." +# ---- BASH_SOURCE guard: everything below only runs when executed, not sourced ---- +if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + return 0 2>/dev/null || exit 0 +fi - # Append auto-allow setup instructions if not already configured - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - SETUP_MSG="${SETUP_MSG} +# Read stdin (Stop hook pipes context as JSON via stdin) +INPUT=$(cat) -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi +# If the stop hook is already active (Claude already responded to a previous block), +# allow the stop to proceed to avoid an infinite block loop. +STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false") +if [ "$STOP_HOOK_ACTIVE" = "true" ]; then + exit 0 +fi - jq -nc --arg reason "$SETUP_MSG" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' - exit 0 +## Per-project tracker keyed on repo root (resolve symlinks so PWD and REPO_ROOT share a prefix) +REPO_ROOT=$(cd "$(git rev-parse --show-toplevel 2>/dev/null)" && pwd -P) || exit 0 +cd "$(pwd -P)" + +# --- Check if codeflash is already auto-allowed in .claude/settings.json --- +CODEFLASH_AUTO_ALLOWED="false" +SETTINGS_JSON="$REPO_ROOT/.claude/settings.json" +if [ -f "$SETTINGS_JSON" ]; then + if jq -e '.permissions.allow // [] | any(test("codeflash"))' "$SETTINGS_JSON" >/dev/null 2>&1; then + CODEFLASH_AUTO_ALLOWED="true" fi +fi - # Configured but not installed - if [ "$CODEFLASH_INSTALLED" != "true" ]; then - INSTALL_MSG="JS/TS files were changed in a recent commit, but the codeflash npm package is not installed. +# --- Detect new commits with Python/Java/JS/TS files since session started --- -Install codeflash to optimize the JavaScript/TypeScript code for performance by running: +# Extract transcript_path from hook input to determine session start time +TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null || true) +if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then + exit 0 +fi +TRANSCRIPT_DIR=$(dirname "$TRANSCRIPT_PATH") - ${INSTALL_CMD} +# --- Cheap gate: skip if HEAD hasn't changed since last check --- +CURRENT_HEAD=$(git rev-parse HEAD 2>/dev/null) || exit 0 +LAST_HEAD_FILE="$TRANSCRIPT_DIR/codeflash-last-head" +if [ -f "$LAST_HEAD_FILE" ] && [ "$(cat "$LAST_HEAD_FILE")" = "$CURRENT_HEAD" ]; then + exit 0 +fi +echo "$CURRENT_HEAD" > "$LAST_HEAD_FILE" -in \`${CHECK_DIR}\`, then run \`npx codeflash --subagent\` as a background Bash task (run_in_background: true)." +SESSION_START=$(get_file_birth_time "$TRANSCRIPT_PATH") +if [ -z "$SESSION_START" ] || [ "$SESSION_START" = "0" ]; then + exit 0 +fi - # Append auto-allow setup instructions if not already configured - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - INSTALL_MSG="${INSTALL_MSG} +# Find commits with Python/Java/JS/TS files made after the session started +CHANGED_COMMITS=$(git log --after="@$SESSION_START" --name-only --diff-filter=ACMR --pretty=format: -- '*.py' '*.java' '*.js' '*.ts' '*.jsx' '*.tsx' 2>/dev/null | sort -u | grep -v '^$' || true) +if [ -z "$CHANGED_COMMITS" ]; then + exit 0 +fi -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi +# Dedup: don't trigger twice for the same set of changes across sessions. +SEEN_MARKER="$TRANSCRIPT_DIR/codeflash-seen" - jq -nc --arg reason "$INSTALL_MSG" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' - exit 0 - fi +COMMIT_HASH=$(git log --after="@$SESSION_START" --pretty=format:%H -- '*.py' '*.java' '*.js' '*.ts' '*.jsx' '*.tsx' 2>/dev/null | shasum -a 256 | cut -d' ' -f1) +if [ -f "$SEEN_MARKER" ] && grep -qF "$COMMIT_HASH" "$SEEN_MARKER" 2>/dev/null; then + exit 0 +fi +echo "$COMMIT_HASH" >> "$SEEN_MARKER" - # Build codeflash command for JS/TS — must run from project root - if [ -n "$PROJECT_DIR" ] && [ "$PROJECT_DIR" != "$PWD" ]; then - RUN_CMD="cd $PROJECT_DIR && npx codeflash --subagent" - else - RUN_CMD="npx codeflash --subagent" - fi +# --- From here on, we know there are new commits to optimize --- - MESSAGE="JS/TS files were changed in a recent commit. Run \`${RUN_CMD}\` as a background Bash task (run_in_background: true) to optimize the JavaScript/TypeScript code for performance." +# OAuth login script path +OAUTH_SCRIPT="$(dirname "$0")/oauth-login.sh" - # Append auto-allow setup instructions if not already configured - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - MESSAGE="${MESSAGE} +LOGIN_STEP="" +if ! has_api_key; then + LOGIN_STEP=" +- Run \`${OAUTH_SCRIPT}\` to log in to Codeflash. If it exits with code 0, the key is saved. If it exits with code 2 (headless environment), parse the JSON output for the \`url\` and \`state_file\`, ask the user to visit the URL and provide the authorization code, then run \`${OAUTH_SCRIPT} --exchange-code \` to complete the login." +fi -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi +# ---- Main flow ---- - jq -nc --arg reason "$MESSAGE" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' - exit 0 -fi +detect_any_config -# --- Java project path ----------------------------------------------------- -if [ "$PROJECT_TYPE" = "java" ]; then - INSTALL_CMD="pip install codeflash" +# Source find-venv.sh for Python venv detection (requires CHECK_DIR and REPO_ROOT) +CHECK_DIR="${PROJECT_DIR:-$PWD}" +# shellcheck disable=SC1091 +source "$(dirname "$0")/find-venv.sh" - # Find codeflash binary: check PATH first, then uv run (Java projects don't use venvs) - CODEFLASH_BIN="" - CODEFLASH_INSTALLED="false" - if command -v codeflash >/dev/null 2>&1; then - CODEFLASH_BIN="codeflash" - CODEFLASH_INSTALLED="true" - elif uv run codeflash --version >/dev/null 2>&1; then - CODEFLASH_BIN="uv run codeflash" - CODEFLASH_INSTALLED="true" - fi +if [ "$PROJECT_CONFIGURED" = "true" ]; then + find_codeflash_binary - # Not installed if [ "$CODEFLASH_INSTALLED" != "true" ]; then - INSTALL_MSG="Java files were changed in a recent commit, but codeflash is not installed. - -Install codeflash to optimize the Java code for performance by running: + # Show install instructions (language-agnostic) + INSTALL_MSG="Code was changed in a recent commit, but codeflash is not installed. +${LOGIN_STEP} +Install codeflash to optimize your code for performance: - ${INSTALL_CMD} + pip install codeflash Then run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." + # Append auto-allow if needed if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then INSTALL_MSG="${INSTALL_MSG} Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." fi - jq -nc --arg reason "$INSTALL_MSG" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' + jq -nc --arg reason "$INSTALL_MSG" '{"decision": "block", "reason": $reason}' exit 0 fi - # Not configured - if [ "$PROJECT_CONFIGURED" != "true" ]; then - SETUP_MSG="Java files were changed in a recent commit, but codeflash is not configured yet. - -Run \`${CODEFLASH_BIN} init --yes\` in \`${CHECK_DIR}\` to automatically detect and configure the Java project, then run \`${CODEFLASH_BIN} --subagent\` as a background Bash task (run_in_background: true) to optimize the code for performance." + # Check for API key before running codeflash + if ! has_api_key; then + LOGIN_MSG="Code was changed in a recent commit, but no Codeflash API key was found. - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - SETUP_MSG="${SETUP_MSG} +Run \`${OAUTH_SCRIPT}\` to log in to Codeflash. If it exits with code 0, the key is saved. If it exits with code 2 (headless environment), parse the JSON output for the \`url\` and \`state_file\`, ask the user to visit the URL and provide the authorization code, then run \`${OAUTH_SCRIPT} --exchange-code \` to complete the login. -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi +After login, run \`codeflash --subagent\` as a background Bash task (run_in_background: true) to optimize the code." - jq -nc --arg reason "$SETUP_MSG" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' + jq -nc --arg reason "$LOGIN_MSG" '{"decision": "block", "reason": $reason}' exit 0 fi - # Everything ready — build codeflash command for Java + # Build run command if [ -n "$PROJECT_DIR" ] && [ "$PROJECT_DIR" != "$PWD" ]; then RUN_CMD="cd $PROJECT_DIR && $CODEFLASH_BIN --subagent" else RUN_CMD="$CODEFLASH_BIN --subagent" fi - MESSAGE="Java files were changed in a recent commit. Run \`${RUN_CMD}\` as a background Bash task (run_in_background: true) to optimize the Java code for performance." + MESSAGE="Code was changed in a recent commit. Run \`${RUN_CMD}\` as a background Bash task (run_in_background: true) to optimize the code for performance." if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then MESSAGE="${MESSAGE} @@ -347,42 +277,27 @@ Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTING Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." fi - jq -nc --arg reason "$MESSAGE" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' - exit 0 -fi - -# --- Python project path --------------------------------------------------- -if [ "$HAS_PYTHON_CHANGES" != "true" ]; then + jq -nc --arg reason "$MESSAGE" '{"decision": "block", "reason": $reason}' exit 0 fi -INSTALL_CMD="pip install codeflash" +# --- NOT CONFIGURED path: language-aware setup instructions --- +detect_changed_languages -# Only use codeflash from the current virtual environment. -# If no venv is active, try to find and activate one automatically. -# shellcheck disable=SC1091 -source "$(dirname "$0")/find-venv.sh" - -if [ -z "${VIRTUAL_ENV:-}" ]; then - # No venv found — instruct Claude to create one, install codeflash, configure, and run - - # Build config setup instructions if not configured - PYPROJECT_SETUP="" - if [ "$PROJECT_CONFIGURED" != "true" ]; then - if [ -n "$PROJECT_CONFIG_PATH" ]; then - TARGET_MSG="Append to \`$PROJECT_CONFIG_PATH\`" - else - TARGET_MSG="Create \`$REPO_ROOT/pyproject.toml\`" - fi +for LANG in $CHANGED_LANGS; do + # Build per-language setup message + case "$LANG" in + python) + SETUP_MSG="Python files were changed in a recent commit, but codeflash is not configured yet. - PYPROJECT_SETUP=" -3. Automatically determine the following two values by exploring the project's actual directory structure using Glob and Read (do NOT present options to the user — discover the correct paths yourself): - a. The relative path with respect to pyproject.toml to the root of the Python module — the module root is where tests import from. for example, if the module root is abc/ then the tests would be importing code as \`from abc import xyz\`. - b. The relative path with respect to pyproject.toml to the tests folder — look for existing directories named \`tests\` or \`test\`, or folders containing files that start with \`test_\`. If none exist, default to \`tests\`. +Set up codeflash to optimize the Python code for performance. Automatically determine the following by exploring the project's actual directory structure using Glob and Read (do NOT present options to the user -- discover the correct paths yourself): +${LOGIN_STEP} +1. The relative path with respect to pyproject.toml to the root of the Python module -- the module root is where tests import from. for example, if the module root is abc/ then the tests would be importing code as \`from abc import xyz\`. +2. The relative path with respect to pyproject.toml to the tests folder -- look for existing directories named \`tests\` or \`test\`, or folders containing files that start with \`test_\`. If none exist, default to \`tests\`. -4. Once determined: - - If the tests folder does not exist, create it with mkdir -p. - - ${TARGET_MSG} (create the file if it does not exist), adding the following section with the discovered values substituted: +Once determined: +- If the tests folder does not exist, create it with mkdir -p. +- Create or update \`pyproject.toml\` adding the following section with the discovered values substituted: [tool.codeflash] # All paths are relative to this pyproject.toml's directory. @@ -391,77 +306,38 @@ tests-root = \"\" ignore-paths = [] formatter-cmds = [\"disabled\"] -5. Run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." - else - PYPROJECT_SETUP=" -3. Run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." - fi - - # Build auto-allow setup step if not already configured - SETUP_PERMISSIONS_STEP="" - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - SETUP_PERMISSIONS_STEP=" -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi - - VENV_MSG="Python files were changed in a recent commit, but no Python virtual environment was found. - -Set up codeflash to optimize the Python code for performance. Perform the following steps: +Once the configuration is written, run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." + ;; + java) + SETUP_MSG="Java files were changed in a recent commit, but codeflash is not configured yet. ${LOGIN_STEP} -1. Create a virtual environment by running: \`python3 -m venv .venv\` in \`$REPO_ROOT\`, then activate it by running: \`source $REPO_ROOT/.venv/bin/activate\`. - -2. Install codeflash by running: \`${INSTALL_CMD}\`. -${PYPROJECT_SETUP} -${SETUP_PERMISSIONS_STEP}" - - jq -nc --arg reason "$VENV_MSG" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' - exit 0 -fi - -CODEFLASH_BIN="${VIRTUAL_ENV}/bin/codeflash" - -# Check if codeflash is installed in the venv -CODEFLASH_INSTALLED="false" -if [ -x "$CODEFLASH_BIN" ] && "$CODEFLASH_BIN" --version >/dev/null 2>&1; then - CODEFLASH_INSTALLED="true" -fi - -# Check if codeflash is configured in this project -if [ "$PROJECT_CONFIGURED" != "true" ]; then - # Build a human-friendly target path for the setup message - if [ -n "$PROJECT_CONFIG_PATH" ]; then - TARGET_MSG="Append to \`$PROJECT_CONFIG_PATH\`" - else - TARGET_MSG="Create \`$REPO_ROOT/pyproject.toml\`" - fi - - # Include install step if codeflash is not installed - INSTALL_STEP="" - if [ "$CODEFLASH_INSTALLED" != "true" ]; then - INSTALL_STEP=" -- First, install codeflash by running \`${INSTALL_CMD}\` in \`${CHECK_DIR}\`." - fi - - SETUP_MSG="Python files were changed in a recent commit, but codeflash is not configured yet. +Run \`codeflash init --yes\` to automatically detect and configure the Java project, then run \`codeflash --subagent\` as a background Bash task (run_in_background: true) to optimize the code for performance." + ;; + javascript) + SETUP_MSG="JS/TS files were changed in a recent commit, but codeflash is not configured yet. -Set up codeflash to optimize the Python code for performance: +Set up codeflash to optimize the JavaScript/TypeScript code for performance. Automatically determine the following by exploring the project's actual directory structure using Glob and Read (do NOT present options to the user -- discover the correct paths yourself): ${LOGIN_STEP} -Automatically determine the following by exploring the project's actual directory structure using Glob and Read (do NOT present options to the user — discover the correct paths yourself): -1. The relative path with respect to pyproject.toml to the root of the Python module — the module root is where tests import from. for example, if the module root is abc/ then the tests would be importing code as \`from abc import xyz\`. -2. The relative path with respect to pyproject.toml to the tests folder — look for existing directories named \`tests\` or \`test\`, or folders containing files that start with \`test_\`. If none exist, default to \`tests\`. +1. The relative path with respect to package.json to the root of the JS/TS module -- typically \".\" for the root directory or \"src\". +2. The relative path with respect to package.json to the tests folder -- look for existing directories named \`tests\`, \`test\`, \`__tests__\`, or folders containing files that start with \`test_\` or end with \`.test.js\`/\`.spec.ts\`. If none exist, default to \`tests\`. -Once determined:${INSTALL_STEP} +Once determined: - If the tests folder does not exist, create it with mkdir -p. -- ${TARGET_MSG} (create the file if it does not exist), adding the following section with the discovered values substituted: +- Add/update the \"codeflash\" key in \`package.json\`: -[tool.codeflash] -# All paths are relative to this pyproject.toml's directory. -module-root = \"\" -tests-root = \"\" -ignore-paths = [] -formatter-cmds = [\"disabled\"] +{ + \"codeflash\": { + \"moduleRoot\": \"\", + \"testsRoot\": \"\", + \"formatterCmds\": [\"disabled\"], + \"ignorePaths\": [\"dist\", \"**/node_modules\", \"**/__tests__\"] + } +} -Once the pyproject.toml configuration is written, run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." +Once the configuration is written, run \`npx codeflash --subagent\` as a background Bash task (run_in_background: true)." + ;; + *) continue ;; + esac # Append auto-allow setup instructions if not already configured if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then @@ -472,55 +348,7 @@ Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTING jq -nc --arg reason "$SETUP_MSG" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' exit 0 -fi - -# Codeflash is configured but not installed in the venv -if [ "$CODEFLASH_INSTALLED" != "true" ]; then - INSTALL_MSG="Python files were changed in a recent commit, but codeflash is not installed in the active virtual environment (\`$VIRTUAL_ENV\`). -${LOGIN_STEP} -Install codeflash to optimize the Python code for performance by running: - - ${INSTALL_CMD} - -in \`${CHECK_DIR}\`, then run \`codeflash --subagent\` as a background Bash task (run_in_background: true)." - - # Append auto-allow setup instructions if not already configured - if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - INSTALL_MSG="${INSTALL_MSG} - -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." - fi - - jq -nc --arg reason "$INSTALL_MSG" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' - exit 0 -fi - -# Check for API key before running codeflash -if ! has_api_key; then - LOGIN_MSG="Python files were changed in a recent commit, but no Codeflash API key was found. - -Run \`${OAUTH_SCRIPT}\` to log in to Codeflash. If it exits with code 0, the key is saved. If it exits with code 2 (headless environment), parse the JSON output for the \`url\` and \`state_file\`, ask the user to visit the URL and provide the authorization code, then run \`${OAUTH_SCRIPT} --exchange-code \` to complete the login. - -After login, run \`codeflash --subagent\` as a background Bash task (run_in_background: true) to optimize the code." - - jq -nc --arg reason "$LOGIN_MSG" '{"decision": "block", "reason": $reason}' - exit 0 -fi - -# Instruct Claude to run codeflash as a background subagent -if [ -n "$PROJECT_DIR" ] && [ "$PROJECT_DIR" != "$PWD" ]; then - RUN_CMD="cd $PROJECT_DIR && $CODEFLASH_BIN --subagent" -else - RUN_CMD="$CODEFLASH_BIN --subagent" -fi - -MESSAGE="Python files were changed in a recent commit. Run \`${RUN_CMD}\` as a background Bash task (run_in_background: true) to optimize the Python code for performance." - -# Append auto-allow setup instructions if not already configured -if [ "$CODEFLASH_AUTO_ALLOWED" != "true" ]; then - MESSAGE="${MESSAGE} - -Then, add \`Bash(*codeflash*)\` to the \`permissions.allow\` array in \`$SETTINGS_JSON\` (create the file and \`.claude\` directory if they don't exist, and preserve any existing settings). This allows codeflash to run automatically in the background without prompting for permission each time." -fi +done -jq -nc --arg reason "$MESSAGE" '{"decision": "block", "reason": $reason, "systemMessage": $reason}' +# No recognized languages in changed files -- exit silently +exit 0 diff --git a/tests/helpers/setup.bash b/tests/helpers/setup.bash index 4302c5c..43ab615 100644 --- a/tests/helpers/setup.bash +++ b/tests/helpers/setup.bash @@ -216,6 +216,28 @@ MOCK chmod +x "$MOCK_BIN/npx" } +# Create a mock uv binary in MOCK_BIN that fails for codeflash. +# This prevents find_codeflash_binary from finding codeflash via `uv run`. +setup_mock_uv_no_codeflash() { + mkdir -p "$MOCK_BIN" + cat > "$MOCK_BIN/uv" << 'MOCK' +#!/bin/bash +if [[ "$1" == "run" && "$2" == "codeflash" ]]; then + exit 1 +fi +exit 127 +MOCK + chmod +x "$MOCK_BIN/uv" +} + +# Minimal PATH for "not installed" tests: mock bin + system essentials only. +# Prevents finding codeflash via host uv/npx/PATH. +not_installed_path() { + setup_mock_uv_no_codeflash + setup_mock_npx false + echo "$MOCK_BIN:/usr/bin:/bin" +} + # --------------------------------------------------------------------------- # Hook runner # --------------------------------------------------------------------------- diff --git a/tests/suggest_optimize.bats b/tests/suggest_optimize.bats new file mode 100644 index 0000000..2d9bde2 --- /dev/null +++ b/tests/suggest_optimize.bats @@ -0,0 +1,193 @@ +#!/usr/bin/env bats + +load test_helper + +# --- detect_any_config tests --- + +@test "detect_any_config finds codeflash.toml" { + mkdir -p "$TEST_DIR" + printf '[tool.codeflash]\nmodule-root = "src/main/java"\n' > "$TEST_DIR/codeflash.toml" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "true" ] + [[ "$FOUND_CONFIGS" == *"codeflash.toml"* ]] + [ "$PROJECT_DIR" = "$TEST_DIR" ] +} + +@test "detect_any_config finds pyproject.toml" { + mkdir -p "$TEST_DIR" + printf '[tool.codeflash]\nmodule-root = "src"\n' > "$TEST_DIR/pyproject.toml" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "true" ] + [[ "$FOUND_CONFIGS" == *"pyproject.toml"* ]] +} + +@test "detect_any_config finds package.json with codeflash key" { + mkdir -p "$TEST_DIR" + echo '{"codeflash": {"moduleRoot": "src"}}' > "$TEST_DIR/package.json" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "true" ] + [[ "$FOUND_CONFIGS" == *"package.json"* ]] +} + +@test "detect_any_config finds multiple config types" { + mkdir -p "$TEST_DIR" + printf '[tool.codeflash]\nmodule-root = "src"\n' > "$TEST_DIR/pyproject.toml" + printf '[tool.codeflash]\nmodule-root = "src/main/java"\n' > "$TEST_DIR/codeflash.toml" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "true" ] + [[ "$FOUND_CONFIGS" == *"pyproject.toml"* ]] + [[ "$FOUND_CONFIGS" == *"codeflash.toml"* ]] +} + +@test "detect_any_config returns false when no config found" { + mkdir -p "$TEST_DIR" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "false" ] + [ -z "$FOUND_CONFIGS" ] +} + +@test "detect_any_config skips package.json without codeflash key" { + mkdir -p "$TEST_DIR" + echo '{"name": "my-project", "version": "1.0.0"}' > "$TEST_DIR/package.json" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "false" ] +} + +@test "detect_any_config skips pyproject.toml without codeflash section" { + mkdir -p "$TEST_DIR" + printf '[tool.black]\nline-length = 120\n' > "$TEST_DIR/pyproject.toml" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "false" ] +} + +@test "detect_any_config finds all three config types" { + mkdir -p "$TEST_DIR" + printf '[tool.codeflash]\nmodule-root = "src"\n' > "$TEST_DIR/pyproject.toml" + printf '[tool.codeflash]\nmodule-root = "src/main/java"\n' > "$TEST_DIR/codeflash.toml" + echo '{"codeflash": {"moduleRoot": "src"}}' > "$TEST_DIR/package.json" + load_hook_functions + detect_any_config + [ "$PROJECT_CONFIGURED" = "true" ] + [[ "$FOUND_CONFIGS" == *"codeflash.toml"* ]] + [[ "$FOUND_CONFIGS" == *"pyproject.toml"* ]] + [[ "$FOUND_CONFIGS" == *"package.json"* ]] +} + +# --- find_codeflash_binary tests --- + +@test "find_codeflash_binary finds binary in PATH" { + mkdir -p "$TEST_DIR/bin" + printf '#!/bin/bash\necho "codeflash 1.0"\n' > "$TEST_DIR/bin/codeflash" + chmod +x "$TEST_DIR/bin/codeflash" + export PATH="$TEST_DIR/bin:$PATH" + load_hook_functions + find_codeflash_binary + [ "$CODEFLASH_INSTALLED" = "true" ] + [ "$CODEFLASH_BIN" = "codeflash" ] +} + +@test "find_codeflash_binary finds binary in venv" { + mkdir -p "$TEST_DIR/venv/bin" + printf '#!/bin/bash\necho "codeflash 1.0"\n' > "$TEST_DIR/venv/bin/codeflash" + chmod +x "$TEST_DIR/venv/bin/codeflash" + export VIRTUAL_ENV="$TEST_DIR/venv" + load_hook_functions + find_codeflash_binary + [ "$CODEFLASH_INSTALLED" = "true" ] + [ "$CODEFLASH_BIN" = "$TEST_DIR/venv/bin/codeflash" ] +} + +@test "find_codeflash_binary reports not installed when missing" { + load_hook_functions + # Save PATH and use an empty directory so codeflash/uv/npx are all unavailable + local saved_path="$PATH" + mkdir -p "$TEST_DIR/empty_bin" + export PATH="$TEST_DIR/empty_bin" + unset VIRTUAL_ENV + hash -r 2>/dev/null || true + find_codeflash_binary + # Restore PATH before assertions so teardown can work + export PATH="$saved_path" + [ "$CODEFLASH_INSTALLED" = "false" ] + [ -z "$CODEFLASH_BIN" ] +} + +@test "find_codeflash_binary prefers venv over PATH" { + mkdir -p "$TEST_DIR/venv/bin" + printf '#!/bin/bash\necho "venv codeflash"\n' > "$TEST_DIR/venv/bin/codeflash" + chmod +x "$TEST_DIR/venv/bin/codeflash" + mkdir -p "$TEST_DIR/bin" + printf '#!/bin/bash\necho "path codeflash"\n' > "$TEST_DIR/bin/codeflash" + chmod +x "$TEST_DIR/bin/codeflash" + export VIRTUAL_ENV="$TEST_DIR/venv" + export PATH="$TEST_DIR/bin:$PATH" + load_hook_functions + find_codeflash_binary + [ "$CODEFLASH_INSTALLED" = "true" ] + [ "$CODEFLASH_BIN" = "$TEST_DIR/venv/bin/codeflash" ] +} + +# --- detect_changed_languages tests --- + +@test "detect_changed_languages detects python" { + export CHANGED_COMMITS="src/main.py +tests/test_utils.py" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"python"* ]] +} + +@test "detect_changed_languages detects java" { + export CHANGED_COMMITS="src/Main.java" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"java"* ]] +} + +@test "detect_changed_languages detects javascript from ts and jsx" { + export CHANGED_COMMITS="src/App.tsx +src/utils.js" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"javascript"* ]] +} + +@test "detect_changed_languages detects mixed languages" { + export CHANGED_COMMITS="src/main.py +src/Main.java +src/app.ts" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"python"* ]] + [[ "$CHANGED_LANGS" == *"java"* ]] + [[ "$CHANGED_LANGS" == *"javascript"* ]] +} + +@test "detect_changed_languages returns empty for no recognized files" { + export CHANGED_COMMITS="README.md +Makefile" + load_hook_functions + detect_changed_languages + [ -z "$CHANGED_LANGS" ] +} + +@test "detect_changed_languages detects js from .jsx files" { + export CHANGED_COMMITS="src/Component.jsx" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"javascript"* ]] +} + +@test "detect_changed_languages detects js from .tsx files" { + export CHANGED_COMMITS="src/Page.tsx" + load_hook_functions + detect_changed_languages + [[ "$CHANGED_LANGS" == *"javascript"* ]] +} diff --git a/tests/test_helper.bash b/tests/test_helper.bash new file mode 100644 index 0000000..5fe0750 --- /dev/null +++ b/tests/test_helper.bash @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# Shared setup for suggest-optimize.sh bats tests + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +HOOK_SCRIPT="$SCRIPT_DIR/scripts/suggest-optimize.sh" + +setup() { + TEST_DIR="$(mktemp -d)" + export REPO_ROOT="$TEST_DIR" + cd "$TEST_DIR" || return 1 +} + +teardown() { + rm -rf "$TEST_DIR" +} + +# Source only the function definitions (guard prevents main flow) +load_hook_functions() { + source "$HOOK_SCRIPT" +} diff --git a/tests/test_suggest_optimize.bats b/tests/test_suggest_optimize.bats index 2e74e80..d5760fd 100755 --- a/tests/test_suggest_optimize.bats +++ b/tests/test_suggest_optimize.bats @@ -147,8 +147,10 @@ setup() { add_python_commit create_pyproject true create_fake_venv "$REPO/.venv" false + local restricted_path + restricted_path=$(not_installed_path) - run run_hook false "VIRTUAL_ENV=$REPO/.venv" + run run_hook false "VIRTUAL_ENV=$REPO/.venv" "PATH=$restricted_path" assert_block assert_reason_contains "pip install codeflash" } @@ -173,12 +175,12 @@ setup() { # Setup: pyproject.toml without [tool.codeflash]. Fake venv WITHOUT codeflash # binary. VIRTUAL_ENV set. -# Validates: When both installation and configuration are missing, the hook -# should instruct Claude to both install codeflash and set up the -# config. The install step is embedded within the setup instructions. -# Expected: Block with reason containing both "[tool.codeflash]" (setup) and -# "install codeflash" (installation). -@test "python: NOT configured + NOT installed → setup + install prompt" { +# Validates: When configuration is missing, the unified hook takes the +# NOT CONFIGURED path with per-language setup instructions. +# The setup message includes the [tool.codeflash] config template. +# Expected: Block with reason containing "[tool.codeflash]" (config template) +# and "module-root" (config field). +@test "python: NOT configured + NOT installed → setup prompt" { add_python_commit create_pyproject false create_fake_venv "$REPO/.venv" false @@ -186,44 +188,41 @@ setup() { run run_hook false "VIRTUAL_ENV=$REPO/.venv" assert_block assert_reason_contains "[tool.codeflash]" - assert_reason_contains "install codeflash" + assert_reason_contains "module-root" } # Setup: pyproject.toml with [tool.codeflash]. No .venv or venv directory -# anywhere. VIRTUAL_ENV not set. -# Validates: Without a virtual environment, codeflash cannot run. The hook -# should instruct the user to create a venv, install codeflash in it, -# and restart Claude Code from within the activated environment. -# Expected: Block with reason containing "virtual environment" and -# "python3 -m venv" (the venv creation command). -@test "python: no venv + configured → create venv prompt" { +# anywhere. VIRTUAL_ENV not set. No codeflash in PATH/uv/npx. +# Validates: The unified hook uses find_codeflash_binary which checks venv, +# PATH, uv run, and npx. When none are found, it shows a generic +# "not installed" message with install instructions. +# Expected: Block with reason containing "pip install codeflash". +@test "python: no venv + configured → install prompt" { add_python_commit create_pyproject true + local restricted_path + restricted_path=$(not_installed_path) # No venv created, no VIRTUAL_ENV set - run run_hook false + run run_hook false "PATH=$restricted_path" assert_block - assert_reason_contains "virtual environment" - assert_reason_contains "python3 -m venv" + assert_reason_contains "pip install codeflash" } # Setup: pyproject.toml WITHOUT [tool.codeflash]. No venv anywhere. # VIRTUAL_ENV not set. -# Validates: The worst case — nothing is set up. The hook should instruct the -# user to create a venv, install codeflash, AND set up the config. -# The setup instructions (with [tool.codeflash] template) are included -# alongside the venv creation steps. -# Expected: Block with reason containing "virtual environment", -# "python3 -m venv", and "[tool.codeflash]". -@test "python: no venv + NOT configured → create venv + setup prompt" { +# Validates: When nothing is set up, the unified hook takes the NOT CONFIGURED +# path with per-language setup instructions including the config +# template. Install is handled implicitly when the user tries to run. +# Expected: Block with reason containing "[tool.codeflash]" and "module-root". +@test "python: no venv + NOT configured → setup prompt" { add_python_commit create_pyproject false run run_hook false assert_block - assert_reason_contains "virtual environment" - assert_reason_contains "python3 -m venv" assert_reason_contains "[tool.codeflash]" + assert_reason_contains "module-root" } # Setup: pyproject.toml with [tool.codeflash]. Fake venv at $REPO/.venv with @@ -231,14 +230,14 @@ setup() { # Validates: The hook sources find-venv.sh which searches CHECK_DIR/.venv, # CHECK_DIR/venv, REPO_ROOT/.venv, REPO_ROOT/venv for an activate # script. It should find .venv, activate it (setting VIRTUAL_ENV), -# and then proceed as if the venv was active from the start. +# and then find_codeflash_binary picks up the venv binary. # Expected: Block with reason containing "codeflash --subagent" (same as the # happy path — auto-discovery is transparent). @test "python: auto-discovers .venv when VIRTUAL_ENV not set" { add_python_commit create_pyproject true create_fake_venv "$REPO/.venv" true - # Don't pass VIRTUAL_ENV — script should find .venv itself + # Don't pass VIRTUAL_ENV — find-venv.sh should discover .venv run run_hook false assert_block @@ -252,10 +251,10 @@ setup() { # Setup: package.json with "codeflash" config key. Mock npx that returns # success for `codeflash --version`. PATH includes mock bin. # One .js file committed after session start. No pyproject.toml. -# Validates: The JS "happy path" — package.json is configured, codeflash npm -# package is available via npx. The hook instructs Claude to run -# `npx codeflash --subagent` in the background. -# Expected: Block with reason containing "npx codeflash --subagent" and +# Validates: The JS "happy path" — package.json is configured, codeflash is +# available. The unified hook instructs Claude to run +# `codeflash --subagent` in the background. +# Expected: Block with reason containing "codeflash --subagent" and # "run_in_background". @test "js: configured + codeflash installed → run codeflash" { add_js_commit @@ -264,33 +263,33 @@ setup() { run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block - assert_reason_contains "npx codeflash --subagent" + assert_reason_contains "codeflash --subagent" assert_reason_contains "run_in_background" } # Setup: package.json with "codeflash" key. Mock npx returns failure for # `codeflash --version` (package not installed). One .js commit. -# Validates: When codeflash is configured in package.json but the npm package -# is not installed, the hook should prompt to install it as a dev -# dependency before running. -# Expected: Block with reason containing "npm install --save-dev codeflash". +# Validates: When codeflash is configured but the binary is not found by +# find_codeflash_binary, the unified hook shows a generic install +# message with "pip install codeflash". +# Expected: Block with reason containing "pip install codeflash". @test "js: configured + NOT installed → install prompt" { add_js_commit create_package_json true - setup_mock_npx false + local restricted_path + restricted_path=$(not_installed_path) - run run_hook false "PATH=$MOCK_BIN:$PATH" + run run_hook false "PATH=$restricted_path" assert_block - assert_reason_contains "npm install --save-dev codeflash" + assert_reason_contains "pip install codeflash" } # Setup: package.json exists but has NO "codeflash" key. Mock npx returns # success (codeflash is installed). One .js commit. -# Validates: When codeflash is installed but not configured, the hook should -# instruct Claude to discover project structure and add the "codeflash" -# config key to package.json with moduleRoot, testsRoot, etc. -# Expected: Block with reason containing "moduleRoot" and "testsRoot" -# (the config fields to be added to package.json). +# Validates: When codeflash is not configured, the unified hook takes the +# NOT CONFIGURED path and shows per-language JS/TS setup instructions +# with the package.json config template. +# Expected: Block with reason containing "moduleRoot" and "testsRoot". @test "js: NOT configured + installed → setup prompt" { add_js_commit create_package_json false @@ -304,13 +303,11 @@ setup() { # Setup: package.json without "codeflash" key. Mock npx fails (not installed). # One .js commit. -# Validates: When both installation and configuration are missing for a JS -# project. The setup message should include an install step -# ("npm install --save-dev codeflash") embedded within the broader -# config setup instructions. -# Expected: Block with reason containing both "moduleRoot" (setup) and -# "npm install --save-dev codeflash" (installation). -@test "js: NOT configured + NOT installed → setup + install prompt" { +# Validates: When configuration is missing, the unified hook takes the +# NOT CONFIGURED path regardless of installation state. Shows +# JS/TS setup instructions with config template. +# Expected: Block with reason containing "moduleRoot" and "testsRoot". +@test "js: NOT configured + NOT installed → setup prompt" { add_js_commit create_package_json false setup_mock_npx false @@ -318,15 +315,14 @@ setup() { run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block assert_reason_contains "moduleRoot" - assert_reason_contains "npm install --save-dev codeflash" + assert_reason_contains "testsRoot" } # Setup: Configured package.json + mock npx. Commit touches a .ts file # instead of .js. -# Validates: TypeScript files (*.ts) are detected by the git log filter and -# route through the JS project path (since package.json is the -# project config). The hook should treat .ts the same as .js. -# Expected: Block with reason containing "npx codeflash --subagent". +# Validates: TypeScript files (*.ts) are detected by the git log filter. +# The unified hook finds package.json config and runs codeflash. +# Expected: Block with reason containing "codeflash --subagent". @test "js: typescript file triggers JS path" { add_ts_commit "utils.ts" create_package_json true @@ -334,14 +330,14 @@ setup() { run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block - assert_reason_contains "npx codeflash --subagent" + assert_reason_contains "codeflash --subagent" } # Setup: Configured package.json + mock npx. Commit touches a .jsx file. # Validates: JSX files (*.jsx) are also detected by the git log filter -# (-- '*.jsx') and processed via the JS path. Ensures React -# component files trigger optimization. -# Expected: Block with reason containing "npx codeflash --subagent". +# (-- '*.jsx'). The unified hook finds package.json config and +# runs codeflash. +# Expected: Block with reason containing "codeflash --subagent". @test "js: jsx file triggers JS path" { add_js_commit "Component.jsx" create_package_json true @@ -349,7 +345,7 @@ setup() { run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block - assert_reason_contains "npx codeflash --subagent" + assert_reason_contains "codeflash --subagent" } # ═══════════════════════════════════════════════════════════════════════════════ @@ -391,24 +387,22 @@ setup() { } # Setup: Fully configured JS project. No .claude/settings.json exists. -# Validates: Same as the Python auto-allow test, but for JS projects. The -# auto-allow logic is shared (checked at script top before branching -# on project type), but the instructions are appended separately in -# each path. This verifies the JS path also appends them. +# Validates: The unified hook appends auto-allow instructions when +# .claude/settings.json doesn't have codeflash permitted. # Expected: Block reason contains "permissions.allow". @test "js: includes auto-allow instructions when settings.json missing" { add_js_commit create_package_json true setup_mock_npx true - run run_hook false "PATH=$MOCK_BIN:$PATH" + run run_hook false "PATH=$MOCK_BIN:$PATH" "CODEFLASH_API_KEY=cf-test-key" assert_block assert_reason_contains "permissions.allow" } # Setup: Fully configured JS project. .claude/settings.json has # "Bash(*codeflash*)" in permissions.allow. -# Validates: JS path correctly omits auto-allow instructions when already set. +# Validates: Unified hook correctly omits auto-allow instructions when already set. # Expected: Block reason does NOT contain "permissions.allow". @test "js: omits auto-allow when already configured" { add_js_commit @@ -416,7 +410,7 @@ setup() { setup_mock_npx true create_auto_allow - run run_hook false "PATH=$MOCK_BIN:$PATH" + run run_hook false "PATH=$MOCK_BIN:$PATH" "CODEFLASH_API_KEY=cf-test-key" assert_block assert_reason_not_contains "permissions.allow" } @@ -428,13 +422,11 @@ setup() { # Setup: BOTH pyproject.toml (with [tool.codeflash]) and package.json (with # "codeflash" key) exist in the same directory. Fake venv with # codeflash installed. One .py commit. -# Validates: The detect_project function checks pyproject.toml before -# package.json at each directory level. When both exist, the Python -# path should be chosen. This ensures Python projects with a -# package.json (e.g., for JS tooling) don't accidentally take the -# JS path. -# Expected: Block with "codeflash --subagent" (bare, Python-style) and -# NOT "npx" (which would indicate the JS path). +# Validates: The unified detect_any_config finds both configs. When the project +# is configured, find_codeflash_binary locates the venv binary. +# The hook fires a single `codeflash --subagent` — the CLI handles +# multi-language dispatch. +# Expected: Block with "codeflash --subagent" and "run_in_background". @test "pyproject.toml takes precedence over package.json in same directory" { add_python_commit create_pyproject true @@ -443,16 +435,14 @@ setup() { run run_hook false "VIRTUAL_ENV=$REPO/.venv" assert_block - # Python path: uses bare codeflash, not npx assert_reason_contains "codeflash --subagent" - assert_reason_not_contains "npx" } # Setup: Only package.json exists (no pyproject.toml). Configured with # "codeflash" key. Mock npx available. One .js commit. -# Validates: When pyproject.toml is absent, detect_project correctly falls -# through to package.json and identifies the project as JS/TS. -# Expected: Block with "npx codeflash --subagent" (JS-style invocation). +# Validates: When pyproject.toml is absent, detect_any_config correctly finds +# package.json. The unified hook runs codeflash --subagent. +# Expected: Block with "codeflash --subagent". @test "detects package.json when no pyproject.toml exists" { add_js_commit # Only package.json, no pyproject.toml @@ -461,5 +451,5 @@ setup() { run run_hook false "PATH=$MOCK_BIN:$PATH" assert_block - assert_reason_contains "npx codeflash --subagent" + assert_reason_contains "codeflash --subagent" } \ No newline at end of file