diff --git a/cli/bash/bin/README.md b/cli/bash/bin/README.md index da145bc..c45fb13 100644 --- a/cli/bash/bin/README.md +++ b/cli/bash/bin/README.md @@ -31,21 +31,31 @@ Behavior: Before sourcing the command script, `bash-wrapper`: +- sources `../../env/banyanenv.sh` to initialize the shared CLI environment - resolves the repository, CLI, and Bash root directories - exports wrapper metadata: - `BANYAN_REPO_ROOT` - `BANYAN_CLI_ROOT` - `BANYAN_BASH_ROOT` - `BANYAN_BASH_BIN_DIR` + - `BANYAN_CLI_ENV_SCRIPT` - `BANYAN_BASH_COMMAND_NAME` - `BANYAN_BASH_COMMAND_DIR` - `BANYAN_BASH_COMMAND_SCRIPT` - preloads `../lib/std/lib_std.sh` -That means command scripts can use the stdlib helpers without sourcing `lib_std.sh` themselves. +That means command scripts inherit both the shared environment and the stdlib helpers without having to source them directly. The wrapper also sets `BANYAN_BASH_BOOTSTRAP_SOURCE` before loading the stdlib so stdlib path detection still treats the command script as the real caller. +`banyanenv.sh` is also meant to be sourced from a user's shell startup file: + +```bash +source /path/to/banyanlabs/cli/env/banyanenv.sh +``` + +That keeps interactive shells and wrapper-launched commands on the same environment contract. + ## Examples Direct dispatch: diff --git a/cli/bash/bin/bash-wrapper b/cli/bash/bin/bash-wrapper index c6c3406..e3f2a10 100755 --- a/cli/bash/bin/bash-wrapper +++ b/cli/bash/bin/bash-wrapper @@ -68,7 +68,7 @@ EOF } main() { - local invoked_as wrapper_path bin_dir bash_root cli_root repo_root commands_dir stdlib_path + local invoked_as wrapper_path bin_dir bash_root cli_root repo_root commands_dir stdlib_path env_script local command_name command_dir command_script invoked_as="$(basename "$0")" @@ -107,6 +107,17 @@ main() { ;; esac + env_script="$cli_root/env/banyanenv.sh" + [[ -f "$env_script" ]] || die "Required environment bootstrap '$env_script' was not found." + source "$env_script" || die "Failed to initialize Banyan CLI environment from '$env_script'." + + bash_root="${BANYAN_BASH_ROOT:-$bash_root}" + cli_root="${BANYAN_CLI_ROOT:-$cli_root}" + repo_root="${BANYAN_REPO_ROOT:-$repo_root}" + bin_dir="${BANYAN_BASH_BIN_DIR:-$bin_dir}" + commands_dir="${BANYAN_BASH_COMMANDS_DIR:-$commands_dir}" + env_script="${BANYAN_CLI_ENV_SCRIPT:-$env_script}" + command_dir="$commands_dir/$command_name" if [[ -f "$command_dir/main.sh" ]]; then command_script="$command_dir/main.sh" @@ -120,6 +131,7 @@ main() { export BANYAN_CLI_ROOT="$cli_root" export BANYAN_BASH_ROOT="$bash_root" export BANYAN_BASH_BIN_DIR="$bin_dir" + export BANYAN_CLI_ENV_SCRIPT="$env_script" export BANYAN_BASH_COMMAND_NAME="$command_name" export BANYAN_BASH_COMMAND_DIR="$command_dir" export BANYAN_BASH_COMMAND_SCRIPT="$command_script" diff --git a/cli/bash/bin/tests/bash-wrapper.bats b/cli/bash/bin/tests/bash-wrapper.bats index 3b2a033..9b03c03 100644 --- a/cli/bash/bin/tests/bash-wrapper.bats +++ b/cli/bash/bin/tests/bash-wrapper.bats @@ -4,8 +4,12 @@ load ../../tests/test_helper.bash create_bare_wrapper_layout() { local layout_root="$1" + local cli_root - mkdir -p "$layout_root/bin" "$layout_root/commands" "$layout_root/lib/std" + cli_root="$(dirname "$layout_root")" + + mkdir -p "$layout_root/bin" "$layout_root/commands" "$layout_root/lib/std" "$cli_root/env" "$cli_root/python" + cp "$BANYAN_REPO_ROOT/cli/env/banyanenv.sh" "$cli_root/env/banyanenv.sh" cp "$BANYAN_BASH_DIR/bin/bash-wrapper" "$layout_root/bin/bash-wrapper" cp "$BANYAN_BASH_DIR/lib/std/lib_std.sh" "$layout_root/lib/std/lib_std.sh" chmod +x "$layout_root/bin/bash-wrapper" @@ -26,7 +30,13 @@ printf 'orig_args=%s\n' "${__SCRIPT_ARGS__[*]:-}" printf 'command=%s\n' "${BANYAN_BASH_COMMAND_NAME:-}" printf 'repo=%s\n' "${BANYAN_REPO_ROOT:-}" printf 'bash_root=%s\n' "${BANYAN_BASH_ROOT:-}" +printf 'bin_dir=%s\n' "${BANYAN_BASH_BIN_DIR:-}" +printf 'env_script=%s\n' "${BANYAN_CLI_ENV_SCRIPT:-}" printf 'script=%s\n' "${BANYAN_BASH_COMMAND_SCRIPT:-}" +case ":$PATH:" in + *":${BANYAN_BASH_BIN_DIR:-__missing__}:"*) printf 'path_has_bin=yes\n' ;; + *) printf 'path_has_bin=no\n' ;; +esac printf 'argv=%s\n' "$*" EOF chmod +x "$layout_root/commands/$command_name/$command_script_name" @@ -35,12 +45,14 @@ EOF @test "bash-wrapper dispatches directly to commands//main.sh" { local repo_root="$BATS_TEST_TMPDIR/repo" local layout="$repo_root/cli/bash" - local expected_repo_root expected_bash_root expected_script_path + local expected_repo_root expected_bash_root expected_bin_dir expected_env_script expected_script_path local expected_command_dir create_wrapper_layout "$layout" demo expected_repo_root="$(cd "$repo_root" && pwd -P)" expected_bash_root="$(cd "$layout" && pwd -P)" + expected_bin_dir="$(cd "$layout/bin" && pwd -P)" + expected_env_script="$(cd "$repo_root/cli/env" && pwd -P)/banyanenv.sh" expected_command_dir="$(cd "$layout/commands/demo" && pwd -P)" expected_script_path="$(cd "$layout/commands/demo" && pwd -P)/main.sh" @@ -52,7 +64,10 @@ EOF [[ "$output" == *"command=demo"* ]] [[ "$output" == *"repo=$expected_repo_root"* ]] [[ "$output" == *"bash_root=$expected_bash_root"* ]] + [[ "$output" == *"bin_dir=$expected_bin_dir"* ]] + [[ "$output" == *"env_script=$expected_env_script"* ]] [[ "$output" == *"script=$expected_script_path"* ]] + [[ "$output" == *"path_has_bin=yes"* ]] [[ "$output" == *"argv=alpha beta"* ]] } @@ -193,6 +208,19 @@ EOF [[ "$output" == *"Required stdlib"* ]] } +@test "wrapper errors when banyanenv is missing" { + local repo_root="$BATS_TEST_TMPDIR/repo" + local layout="$repo_root/cli/bash" + + create_wrapper_layout "$layout" demo + rm -f "$repo_root/cli/env/banyanenv.sh" + + run "$layout/bin/bash-wrapper" demo + + [ "$status" -eq 1 ] + [[ "$output" == *"Required environment bootstrap"* ]] +} + @test "wrapper preloads stdlib so commands can call stdlib helpers without sourcing it" { local repo_root="$BATS_TEST_TMPDIR/repo" local layout="$repo_root/cli/bash" diff --git a/cli/env/README.md b/cli/env/README.md new file mode 100644 index 0000000..184f809 --- /dev/null +++ b/cli/env/README.md @@ -0,0 +1,47 @@ +# `cli/env` + +This directory holds the shared CLI environment bootstrap. + +## Purpose + +`banyanenv.sh` defines the common shell environment used by: + +- `cli/bash/bin/bash-wrapper` +- interactive shells that source it from `~/.bashrc` or `~/.zshrc` +- future Bash and Python CLIs that want a single, shared environment contract + +## Usage + +Source it from a shell startup file or from another script: + +```bash +source /path/to/banyanlabs/cli/env/banyanenv.sh +``` + +It must be sourced rather than executed. + +## What It Exports + +- `BANYAN_REPO_ROOT` +- `BANYAN_CLI_ROOT` +- `BANYAN_CLI_ENV_DIR` +- `BANYAN_CLI_ENV_SCRIPT` +- `BANYAN_BASH_ROOT` +- `BANYAN_BASH_BIN_DIR` +- `BANYAN_BASH_LIB_DIR` +- `BANYAN_BASH_COMMANDS_DIR` +- `BANYAN_PYTHON_ROOT` + +It also prepends `cli/bash/bin` to `PATH` when that directory exists, without duplicating the entry on repeated sourcing. + +## Compatibility + +`banyanenv.sh` is designed to work in both Bash and zsh. + +## Tests + +Run the environment bootstrap test suite with: + +```bash +bats cli/env/tests/banyanenv.bats +``` diff --git a/cli/env/banyanenv.sh b/cli/env/banyanenv.sh new file mode 100644 index 0000000..d3fc7f3 --- /dev/null +++ b/cli/env/banyanenv.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash + +# +# banyanenv.sh +# Sets up the Banyan Labs CLI shell environment. +# Source this file from bash-wrapper or from ~/.bashrc / ~/.zshrc: +# source /path/to/banyanlabs/cli/env/banyanenv.sh +# Compatible with both bash and zsh. +# + +banyanenv_error() { + printf 'ERROR: %s\n' "$*" >&2 +} + +banyanenv_is_sourced() { + if [[ -n "${BASH_VERSION:-}" ]]; then + [[ "${BASH_SOURCE[0]}" != "$0" ]] + return + fi + + if [[ -n "${ZSH_VERSION:-}" ]]; then + [[ "$(eval 'printf "%s\n" "${(%):-%x}"')" != "$0" ]] + return + fi + + return 1 +} + +banyanenv_get_source_path() { + if [[ -n "${BASH_VERSION:-}" ]]; then + printf '%s\n' "${BASH_SOURCE[0]}" + return 0 + fi + + if [[ -n "${ZSH_VERSION:-}" ]]; then + eval 'printf "%s\n" "${(%):-%x}"' + return 0 + fi + + return 1 +} + +banyanenv_prepend_path() { + local dir="$1" + + [[ -n "$dir" && -d "$dir" ]] || return 0 + + case ":${PATH:-}:" in + *":$dir:"*) ;; + *) + if [[ -n "${PATH:-}" ]]; then + PATH="$dir:$PATH" + else + PATH="$dir" + fi + export PATH + ;; + esac +} + +banyanenv_main() { + local source_path env_dir cli_root repo_root bash_root python_root + + source_path="$(banyanenv_get_source_path)" || { + banyanenv_error "Unable to determine the path to banyanenv.sh." + return 1 + } + [[ -n "$source_path" ]] || { + banyanenv_error "Unable to determine the path to banyanenv.sh." + return 1 + } + + env_dir="$(cd -- "$(dirname -- "$source_path")" && pwd -P)" || { + banyanenv_error "Unable to resolve cli/env root from '$source_path'." + return 1 + } + cli_root="$(cd -- "$env_dir/.." && pwd -P)" || { + banyanenv_error "Unable to resolve cli root from '$env_dir'." + return 1 + } + repo_root="$(cd -- "$cli_root/.." && pwd -P)" || { + banyanenv_error "Unable to resolve repository root from '$cli_root'." + return 1 + } + + bash_root="$cli_root/bash" + python_root="$cli_root/python" + + export BANYAN_REPO_ROOT="$repo_root" + export BANYAN_CLI_ROOT="$cli_root" + export BANYAN_CLI_ENV_DIR="$env_dir" + export BANYAN_CLI_ENV_SCRIPT="$env_dir/banyanenv.sh" + export BANYAN_BASH_ROOT="$bash_root" + export BANYAN_BASH_BIN_DIR="$bash_root/bin" + export BANYAN_BASH_LIB_DIR="$bash_root/lib" + export BANYAN_BASH_COMMANDS_DIR="$bash_root/commands" + export BANYAN_PYTHON_ROOT="$python_root" + + banyanenv_prepend_path "$BANYAN_BASH_BIN_DIR" + + return 0 +} + +if ! banyanenv_is_sourced; then + banyanenv_error "banyanenv.sh must be sourced, not executed." + banyanenv_error "Use: source /path/to/banyanlabs/cli/env/banyanenv.sh" + exit 1 +fi + +banyanenv_main +_banyanenv_rc=$? +unset -f banyanenv_error banyanenv_is_sourced banyanenv_get_source_path banyanenv_prepend_path banyanenv_main +if [[ $_banyanenv_rc -ne 0 ]]; then + return "$_banyanenv_rc" 2>/dev/null || exit "$_banyanenv_rc" +fi +unset _banyanenv_rc diff --git a/cli/env/tests/banyanenv.bats b/cli/env/tests/banyanenv.bats new file mode 100644 index 0000000..d264dd5 --- /dev/null +++ b/cli/env/tests/banyanenv.bats @@ -0,0 +1,108 @@ +#!/usr/bin/env bats + +load ../../bash/tests/test_helper.bash + +readonly BANYAN_ENV_SCRIPT="$BANYAN_REPO_ROOT/cli/env/banyanenv.sh" + +create_env_layout() { + local repo_root="$1" + + mkdir -p "$repo_root/cli/env" "$repo_root/cli/bash/bin" "$repo_root/cli/bash/lib" "$repo_root/cli/bash/commands" "$repo_root/cli/python" + cp "$BANYAN_ENV_SCRIPT" "$repo_root/cli/env/banyanenv.sh" +} + +@test "banyanenv must be sourced rather than executed" { + run bash "$BANYAN_ENV_SCRIPT" + + [ "$status" -eq 1 ] + [[ "$output" == *"banyanenv.sh must be sourced, not executed."* ]] +} + +@test "sourcing banyanenv under bash exports shared CLI roots and updates PATH once" { + local repo_root="$BATS_TEST_TMPDIR/repo" + local script="$BATS_TEST_TMPDIR/check-bash-env.sh" + local expected_repo_root expected_cli_root expected_env_dir expected_bash_root expected_bash_bin expected_python_root + + create_env_layout "$repo_root" + expected_repo_root="$(cd "$repo_root" && pwd -P)" + expected_cli_root="$(cd "$repo_root/cli" && pwd -P)" + expected_env_dir="$(cd "$repo_root/cli/env" && pwd -P)" + expected_bash_root="$(cd "$repo_root/cli/bash" && pwd -P)" + expected_bash_bin="$(cd "$repo_root/cli/bash/bin" && pwd -P)" + expected_python_root="$(cd "$repo_root/cli/python" && pwd -P)" + + cat > "$script" <