diff --git a/README.md b/README.md index 48d5069..8f098a5 100644 --- a/README.md +++ b/README.md @@ -232,9 +232,12 @@ to install them and `basectl check --dev` or `basectl doctor --dev` to verify them. On macOS, `basectl setup` sends a best-effort notification when setup completes -or fails. Notifications are skipped during `--dry-run` and never change the -setup exit status. Use `basectl setup --no-notify` or -`BASE_SETUP_NOTIFY=false` to disable them. +or fails after running for at least 30 seconds. Notifications are skipped during +`--dry-run` and never change the setup exit status. Use `basectl setup --notify` +to force a notification for quick runs, `basectl setup --no-notify` or +`BASE_SETUP_NOTIFY=false` to disable notifications, and +`BASE_SETUP_NOTIFY_MIN_SECONDS` to tune the default threshold. When `--notify` +is requested on macOS, Base warns if `osascript` is not available. ## Quick Start diff --git a/cli/bash/commands/basectl/subcommands/setup.sh b/cli/bash/commands/basectl/subcommands/setup.sh index 0640128..0fb59b5 100644 --- a/cli/bash/commands/basectl/subcommands/setup.sh +++ b/cli/bash/commands/basectl/subcommands/setup.sh @@ -17,6 +17,7 @@ Options: --dev Install manifest-declared developer prerequisites. --dry-run Log what would happen without making changes. --manifest Use a specific base_manifest.yaml path. + --notify Force a best-effort macOS notification when setup ends. --no-notify Disable the default best-effort macOS completion notification. --recreate-venv Back up and recreate the project virtual environment. -v Enable DEBUG logging for this subcommand. @@ -100,6 +101,8 @@ base_setup_subcommand_main() { done BASE_SETUP_PROJECT_NAME="$project_name" + BASE_SETUP_START_TIME="$(date +%s)" + export BASE_SETUP_START_TIME export BASE_SETUP_PROJECT_NAME log_debug "Running 'basectl setup' (DRY_RUN=$(setup_is_dry_run && printf true || printf false))." if setup_notifications_enabled; then diff --git a/cli/bash/commands/basectl/subcommands/setup_common.sh b/cli/bash/commands/basectl/subcommands/setup_common.sh index d807d9b..3753dbc 100644 --- a/cli/bash/commands/basectl/subcommands/setup_common.sh +++ b/cli/bash/commands/basectl/subcommands/setup_common.sh @@ -50,12 +50,17 @@ setup_enable_recreate_venv() { setup_enable_notifications() { export BASE_SETUP_NOTIFY=true + export BASE_SETUP_NOTIFY_FORCE=true } setup_disable_notifications() { export BASE_SETUP_NOTIFY=false } +setup_notify_min_seconds() { + printf '%s\n' "${BASE_SETUP_NOTIFY_MIN_SECONDS:-30}" +} + setup_recreate_venv_enabled() { [[ "${BASE_SETUP_RECREATE_VENV:-false}" == true ]] } @@ -64,6 +69,10 @@ setup_notifications_enabled() { [[ "${BASE_SETUP_NOTIFY:-true}" == true ]] } +setup_notifications_forced() { + [[ "${BASE_SETUP_NOTIFY_FORCE:-false}" == true ]] +} + setup_virtualenv_exists() { local venv_dir @@ -153,12 +162,30 @@ setup_recovery_project_layer() { setup_notify_completion() { local exit_code="$1" + local elapsed_seconds=0 local message title + local min_seconds setup_notifications_enabled || return 0 setup_is_dry_run && return 0 [[ "$OSTYPE" == darwin* ]] || return 0 - command -v osascript >/dev/null 2>&1 || return 0 + if ! command -v osascript >/dev/null 2>&1; then + if setup_notifications_forced; then + log_warn "Setup notification was requested, but 'osascript' is not available on this Mac." + fi + return 0 + fi + + min_seconds="$(setup_notify_min_seconds)" + if ! [[ "$min_seconds" =~ ^[0-9]+$ ]]; then + min_seconds=30 + fi + if [[ -n "${BASE_SETUP_START_TIME:-}" && "$BASE_SETUP_START_TIME" =~ ^[0-9]+$ ]]; then + elapsed_seconds=$(($(date +%s) - BASE_SETUP_START_TIME)) + fi + if ! setup_notifications_forced && ((elapsed_seconds < min_seconds)); then + return 0 + fi if ((exit_code == 0)); then title="Base setup complete" diff --git a/cli/bash/commands/basectl/tests/setup.bats b/cli/bash/commands/basectl/tests/setup.bats index f8557b5..e8f4d1f 100644 --- a/cli/bash/commands/basectl/tests/setup.bats +++ b/cli/bash/commands/basectl/tests/setup.bats @@ -453,6 +453,7 @@ EOF [[ "$output" == *"Usage:"* ]] [[ "$output" == *"basectl setup [options]"* ]] [[ "$output" == *"--dev"* ]] + [[ "$output" == *"--notify"* ]] [[ "$output" == *"--no-notify"* ]] [[ "$output" == *"--recreate-venv"* ]] [[ "$output" == *"Prepare the local Base CLI environment on macOS."* ]] @@ -547,6 +548,7 @@ EOF installer="$(create_homebrew_installer_stub)" run_base_command \ + BASE_SETUP_NOTIFY_MIN_SECONDS=999999 \ BASE_SETUP_ALLOW_NONINTERACTIVE_XCODE_INSTALL=true \ BASE_SETUP_HOMEBREW_INSTALLER_SCRIPT="$installer" \ setup @@ -571,7 +573,7 @@ EOF [ -f "$venv_dir/pyvenv.cfg" ] } -@test "basectl setup sends a best-effort success notification by default" { +@test "basectl setup skips notifications for quick successful runs" { local installer create_xcode_stubs @@ -583,12 +585,61 @@ EOF BASE_SETUP_HOMEBREW_INSTALLER_SCRIPT="$installer" \ setup + [ "$status" -eq 0 ] + [ ! -f "$TEST_STATE_DIR/osascript-args" ] +} + +@test "basectl setup sends a best-effort success notification after the threshold" { + local installer + + create_xcode_stubs + create_osascript_stub + installer="$(create_homebrew_installer_stub)" + + run_base_command \ + BASE_SETUP_NOTIFY_MIN_SECONDS=0 \ + BASE_SETUP_ALLOW_NONINTERACTIVE_XCODE_INSTALL=true \ + BASE_SETUP_HOMEBREW_INSTALLER_SCRIPT="$installer" \ + setup + [ "$status" -eq 0 ] [ -f "$TEST_STATE_DIR/osascript-args" ] [[ "$(cat "$TEST_STATE_DIR/osascript-args")" == *"Base setup complete"* ]] [[ "$(cat "$TEST_STATE_DIR/osascript-args")" == *"Base CLI setup completed successfully."* ]] } +@test "basectl setup --notify forces a notification for quick runs" { + local installer + + create_xcode_stubs + create_osascript_stub + installer="$(create_homebrew_installer_stub)" + + run_base_command \ + BASE_SETUP_ALLOW_NONINTERACTIVE_XCODE_INSTALL=true \ + BASE_SETUP_HOMEBREW_INSTALLER_SCRIPT="$installer" \ + setup --notify + + [ "$status" -eq 0 ] + [ -f "$TEST_STATE_DIR/osascript-args" ] + [[ "$(cat "$TEST_STATE_DIR/osascript-args")" == *"Base setup complete"* ]] + [[ "$(cat "$TEST_STATE_DIR/osascript-args")" == *"Base CLI setup completed successfully."* ]] +} + +@test "basectl setup --notify warns when osascript is unavailable on macOS" { + run env \ + HOME="$TEST_HOME" \ + PATH="$TEST_MOCKBIN:$TEST_BASH_BIN_DIR:/bin:/usr/sbin:/sbin" \ + OSTYPE=darwin24 \ + BASE_HOME="$BASE_REPO_ROOT" \ + BASE_SETUP_NOTIFY=true \ + BASE_SETUP_NOTIFY_FORCE=true \ + bash -c 'source "$BASE_HOME/lib/bash/std/lib_std.sh"; source "$BASE_HOME/cli/bash/commands/basectl/subcommands/setup_common.sh"; setup_notify_completion 0' + + [ "$status" -eq 0 ] + [[ "$output" == *"Setup notification was requested, but 'osascript' is not available on this Mac."* ]] +} + @test "basectl setup forwards project setup arguments through the Base venv" { local base_venv_dir="$TEST_HOME/.base.d/base/.venv" local demo_venv_dir="$TEST_HOME/.base.d/demo/.venv" @@ -673,13 +724,13 @@ EOF [[ "$output" == *"Run 'xcode-select --install' in an interactive terminal, complete the installer, then rerun 'basectl setup'."* ]] } -@test "basectl setup sends a best-effort failure notification by default" { +@test "basectl setup sends a best-effort failure notification after the threshold" { create_brew_stub create_xcode_stubs create_osascript_stub touch "$TEST_STATE_DIR/python-installed" - run_base_command setup + run_base_command BASE_SETUP_NOTIFY_MIN_SECONDS=0 setup [ "$status" -eq 1 ] [ -f "$TEST_STATE_DIR/osascript-args" ]