Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions cli/bash/commands/basectl/basectl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ basectl_runtime_slug() {

basectl_run_bundle_project() {
local command="$1"
local argument option_value=0
local argument option_value=0 project_option=0
shift

case "$command" in
Expand All @@ -599,12 +599,24 @@ basectl_run_bundle_project() {

for argument in "$@"; do
if ((option_value)); then
if ((project_option)); then
basectl_runtime_slug "$argument"
return 0
fi
option_value=0
continue
fi
case "$argument" in
--manifest|--format|--repos|--profile|--environment|--config|--log-file|--project|--workspace|--path|--target|--version|--command|--status|--older-than|--keep-last|--since|--until|--last|--lines)
--project)
option_value=1
project_option=1
;;
--manifest|--format|--repos|--profile|--environment|--config|--log-file|--workspace|--path|--target|--version|--command|--status|--older-than|--keep-last|--since|--until|--last|--lines)
option_value=1
;;
--project=*)
basectl_runtime_slug "${argument#*=}"
return 0
;;
--*=*|--*|-*)
;;
Expand Down
52 changes: 17 additions & 35 deletions cli/bash/commands/basectl/subcommands/activate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ _base_project_command_helpers_path="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")"
# shellcheck source=/dev/null
source "$_base_project_command_helpers_path"

import_base_lib arg/lib_arg.sh

base_activate_subcommand_usage() {
cat <<'EOF'
Usage:
basectl activate <project> [options]

Options:
--project <name> Explicitly select a project, including one named "help".
--workspace <path> Workspace directory to scan. Defaults to workspace.root, then BASE_HOME's parent.
--no-cd Preserve the caller's current directory in the project shell.
-v Enable DEBUG logging for this subcommand.
Expand All @@ -29,12 +32,11 @@ base_activate_usage_error() {
}

base_activate_resolve_project() {
local project="$1"
local wrapper="$2"
shift 2
local wrapper="$1"
shift

env -u BASE_PROJECT_VENV_DIR \
"$wrapper" --project base base_projects resolve "$project" "$@" --format command-protocol
"$wrapper" --project base base_projects resolve "$@" --format command-protocol
}

base_activate_project_venv_dir() {
Expand All @@ -56,49 +58,28 @@ base_activate_subcommand_main() {
local project="" wrapper resolve_output activate_shell venv_fix
local resolved_name project_root manifest_path venv_dir shell_rc route_venv_dir uses_uv_manager trust_required
local preserve_cwd="${BASE_ACTIVATE_PRESERVE_CWD:-0}"
local args=()
local parse_args=() args=()

while (($# > 0)); do
case "$1" in
-h|--help|help)
base_activate_subcommand_usage
return 0
;;
-v)
args+=(--debug)
shift
;;
--workspace)
[[ -n "${2:-}" ]] || {
base_activate_usage_error "Option '--workspace' requires an argument."
return $?
}
args+=(--workspace "$2")
shift 2
;;
--workspace=*)
args+=("$1")
shift
;;
--no-cd)
preserve_cwd=1
shift
;;
-*)
base_activate_usage_error "Unknown activate option '$1'."
return $?
;;
*)
if [[ -n "$project" ]]; then
base_activate_usage_error "The 'activate' command accepts exactly one project name."
return $?
fi
project="$1"
parse_args+=("$1")
shift
;;
esac
done

base_project_command_parse_args \
activate base_activate_subcommand_usage base_activate_usage_error \
"The 'activate' command accepts exactly one project name." "${parse_args[@]}" || return $?
[[ "$BASE_PROJECT_COMMAND_HELP_SHOWN" == 1 ]] && return 0
project="$BASE_PROJECT_COMMAND_SELECTED_PROJECT"
args=("${BASE_PROJECT_COMMAND_ARGUMENTS[@]}")

[[ -n "$project" ]] || {
base_activate_usage_error "Project name is required."
return $?
Expand All @@ -107,7 +88,8 @@ base_activate_subcommand_main() {
wrapper="$BASE_HOME/bin/base-wrapper"
[[ -x "$wrapper" ]] || base_std_fatal_error "Base Python wrapper '$wrapper' is missing or is not executable."

resolve_output="$(base_activate_resolve_project "$project" "$wrapper" "${args[@]}")" || return $?
resolve_output="$(base_activate_resolve_project "$wrapper" \
"${BASE_PROJECT_COMMAND_SELECTION_ARGS[@]}" "${args[@]}")" || return $?
base_command_protocol_decode_one project-route "$resolve_output" || {
base_std_fatal_error "Unable to resolve project '$project'."
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ base_project_command_parse_args() {

# shellcheck disable=SC2034 # The selected project is consumed by test.sh and demo.sh.
BASE_PROJECT_COMMAND_PROJECT=""
# shellcheck disable=SC2034 # The selected project is consumed by activate.sh and uninstall.sh.
BASE_PROJECT_COMMAND_SELECTED_PROJECT=""
# shellcheck disable=SC2034 # These shared parse outputs are consumed by test.sh and demo.sh.
BASE_PROJECT_COMMAND_DRY_RUN=0
# shellcheck disable=SC2034 # The parser returns help to its caller without resolving a project.
Expand Down Expand Up @@ -163,6 +165,19 @@ base_project_command_parse_args() {
parser_args+=("--project=$2")
shift 2
;;
--project=*)
[[ -n "${1#*=}" ]] || {
"$usage_error_function" "Option '--project' requires an argument."
return $?
}
[[ -z "$explicit_project" ]] || {
"$usage_error_function" "Option '--project' may be specified only once."
return $?
}
explicit_project="${1#*=}"
parser_args+=("$1")
shift
;;
--dry-run)
parser_args+=("$1")
shift
Expand Down Expand Up @@ -205,6 +220,8 @@ base_project_command_parse_args() {

# shellcheck disable=SC2034 # The selected project is consumed by test.sh and demo.sh.
BASE_PROJECT_COMMAND_PROJECT="$project"
# shellcheck disable=SC2034 # The selected project is consumed by activate.sh and uninstall.sh.
BASE_PROJECT_COMMAND_SELECTED_PROJECT="${explicit_project:-$project}"
# shellcheck disable=SC2034 # Dry-run mode is consumed by test.sh and demo.sh.
BASE_PROJECT_COMMAND_DRY_RUN="$dry_run"
BASE_PROJECT_COMMAND_ARGUMENTS=("${args[@]}")
Expand Down
55 changes: 20 additions & 35 deletions cli/bash/commands/basectl/subcommands/uninstall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
_base_uninstall_subcommand_sourced=1
readonly _base_uninstall_subcommand_sourced

_base_project_command_helpers_path="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)/project_command_helpers.sh"
# shellcheck source=/dev/null
source "$_base_project_command_helpers_path"

import_base_lib arg/lib_arg.sh

base_uninstall_subcommand_usage() {
cat <<'EOF'
Usage:
basectl uninstall <project> [options]
basectl uninstall --all [options]

Options:
--project <name> Explicitly select a project, including one named "help".
--all Remove all Base-managed local state and workspace settings.
--workspace <path>
Workspace directory used to resolve a project name.
Expand Down Expand Up @@ -47,54 +54,36 @@ base_uninstall_run_python() {
}

base_uninstall_subcommand_main() {
local all_projects=0 dry_run=0 verify=0 yes=0 debug=0 workspace="" project="" arg
local -a python_args=()
local all_projects=0 verify=0 yes=0 project=""
local parse_args=() python_args=()

while (($# > 0)); do
arg="$1"
case "$arg" in
-h|--help|help)
base_uninstall_subcommand_usage
return 0
;;
case "$1" in
--all)
all_projects=1
;;
--dry-run)
dry_run=1
;;
--yes)
yes=1
;;
--verify)
verify=1
;;
-v)
debug=1
;;
--workspace)
shift
[[ -n "${1:-}" ]] || {
base_uninstall_usage_error "Option '--workspace' requires an argument."
return $?
}
workspace="$1"
;;
*)
if [[ "$arg" == -* ]]; then
base_uninstall_usage_error "Unknown option '$arg'."
return $?
fi
[[ -z "$project" ]] || {
base_uninstall_usage_error "The 'uninstall' command accepts at most one project name."
return $?
}
project="$arg"
parse_args+=("$1")
;;
esac
shift
done

base_project_command_parse_args \
uninstall base_uninstall_subcommand_usage base_uninstall_usage_error \
"The 'uninstall' command accepts at most one project name." "${parse_args[@]}" || return $?
[[ "$BASE_PROJECT_COMMAND_HELP_SHOWN" == 1 ]] && return 0

local dry_run="$BASE_PROJECT_COMMAND_DRY_RUN"
project="$BASE_PROJECT_COMMAND_SELECTED_PROJECT"
python_args=("${BASE_PROJECT_COMMAND_ARGUMENTS[@]}")

if ((all_projects && ${#project} > 0)); then
base_uninstall_usage_error "Option '--all' cannot be combined with a project name."
return $?
Expand All @@ -111,11 +100,7 @@ base_uninstall_subcommand_main() {
base_uninstall_usage_error "Option '--verify' cannot be combined with '--dry-run' or '--yes'."
return $?
fi
if ((debug)); then
python_args+=(--debug)
fi
((all_projects)) && python_args+=(--all)
[[ -z "$workspace" ]] || python_args+=(--workspace "$workspace")
((dry_run)) && python_args+=(--dry-run)
((yes)) && python_args+=(--yes)
((verify)) && python_args+=(--verify)
Expand Down
43 changes: 43 additions & 0 deletions cli/bash/commands/basectl/tests/activate.bats
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ EOF
[[ "$output" == *"Usage:"* ]]
[[ "$output" == *"basectl activate <project> [options]"* ]]
[[ "$output" == *"--no-cd"* ]]
[[ "$output" == *"--project <name>"* ]]
[[ "$output" == *"interactive Base Bash runtime shell"* ]]
}

Expand All @@ -532,6 +533,48 @@ EOF
[ ! -f "$BASE_CACHE_DIR/base/history/runs.jsonl" ]
}

@test "basectl activate targets a project named help with --project" {
local base_python="$TEST_HOME/.base.d/base/.venv/bin/python"
local workspace="$TEST_TMPDIR/workspace"
local project_root="$workspace/help"
local project_python="$project_root/.venv/bin/python"
local fake_bash="$TEST_TMPDIR/fake-bash"

mkdir -p "$(dirname "$base_python")" "$(dirname "$project_python")"
cat > "$base_python" <<'EOF'
#!/usr/bin/env bash
source "${BASH_ENV:?}"
if [[ "${1:-}" == "-m" && "${2:-}" == "base_projects" && "${3:-}" == "resolve" && \
("${4:-}" == "help" || ("${4:-}" == "--project" && "${5:-}" == "help")) ]]; then
base_test_protocol_project_route help "${BASE_TEST_PROJECT_ROOT:?}" \
"${BASE_TEST_PROJECT_ROOT:?}/base_manifest.yaml" "${BASE_TEST_PROJECT_ROOT:?}/.venv" false false
exit 0
fi
printf 'unexpected activate resolver args: %s\n' "$*" >&2
exit 1
EOF
cat > "$fake_bash" <<'EOF'
#!/usr/bin/env bash
printf 'BASE_PROJECT=%s\n' "$BASE_PROJECT"
printf 'BASE_PROJECT_ROOT=%s\n' "$BASE_PROJECT_ROOT"
EOF
printf '#!/usr/bin/env bash\n' > "$project_python"
chmod +x "$base_python" "$project_python" "$fake_bash"
printf 'project:\n name: help\nartifacts: []\n' > "$project_root/base_manifest.yaml"
workspace="$(cd "$workspace" && pwd -P)"

run env \
HOME="$TEST_HOME" \
PATH="/usr/bin:/bin:/usr/sbin:/sbin" \
BASE_ACTIVATE_SHELL="$fake_bash" \
BASE_TEST_PROJECT_ROOT="$workspace/help" \
"$BASE_REPO_ROOT/bin/basectl" activate --project help

[ "$status" -eq 0 ]
[[ "$output" == *"BASE_PROJECT=help"* ]]
[[ "$output" == *"BASE_PROJECT_ROOT=$workspace/help"* ]]
}

@test "basectl activate rejects non-Bash BASE_ACTIVATE_SHELL before launch" {
local base_python="$TEST_HOME/.base.d/base/.venv/bin/python"
local workspace="$TEST_TMPDIR/workspace"
Expand Down
2 changes: 1 addition & 1 deletion cli/bash/commands/basectl/tests/completions.bats
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ EOF
[ "$status" -eq 0 ]
[[ "$output" == *"complete -F _base_basectl_completion basectl"* ]]
[[ "$output" == *"activate_projects=base demo"* ]]
[[ "$output" == *"activate_options=--workspace --no-cd"* ]]
[[ "$output" == *"activate_options=--workspace --project --no-cd"* ]]
[[ "$output" == *"check_projects=base demo"* ]]
[[ "$output" == *"doctor_projects=base demo explain"* ]]
[[ "$output" == *"doctor_explain_options=--format"* ]]
Expand Down
21 changes: 21 additions & 0 deletions cli/bash/commands/basectl/tests/project-command-helpers.bats
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ manifest=$project_root/base_manifest.yaml" ]
[[ "${#BASE_PROJECT_COMMAND_SELECTION_ARGS[@]}" -eq 2 ]]
[[ "${BASE_PROJECT_COMMAND_SELECTION_ARGS[0]}" == --project ]]
[[ "${BASE_PROJECT_COMMAND_SELECTION_ARGS[1]}" == demo ]]
[[ "$BASE_PROJECT_COMMAND_SELECTED_PROJECT" == demo ]]
[[ "${#BASE_PROJECT_COMMAND_EXTRA_ARGS[@]}" -eq 2 ]]
[[ "${BASE_PROJECT_COMMAND_EXTRA_ARGS[0]}" == "name with spaces" ]]
[[ "${BASE_PROJECT_COMMAND_EXTRA_ARGS[1]}" == "$control_arg" ]]
Expand All @@ -107,6 +108,26 @@ manifest=$project_root/base_manifest.yaml" ]
[[ "$output" == *"argument boundaries preserved"* ]]
}

@test "project command parser accepts equals-form explicit project selection" {
run env \
BASE_HOME="$BASE_REPO_ROOT" \
BASE_BASH_LIBS_DIR="${BASE_BASH_LIBS_DIR:-}" \
bash -c '
source "$BASE_HOME/base_init.sh"
import_base_lib arg/lib_arg.sh
source "$BASE_HOME/cli/bash/commands/basectl/subcommands/project_command_helpers.sh"
base_command_usage() { :; }
base_command_usage_error() { printf "ERROR:%s\n" "$*" >&2; return 2; }
base_project_command_parse_args activate base_command_usage base_command_usage_error \
"The activate command accepts one project name." --project=help || exit $?
[[ "$BASE_PROJECT_COMMAND_SELECTED_PROJECT" == help ]]
[[ "${BASE_PROJECT_COMMAND_SELECTION_ARGS[0]}" == --project ]]
[[ "${BASE_PROJECT_COMMAND_SELECTION_ARGS[1]}" == help ]]
'

[ "$status" -eq 0 ]
}

@test "project command parser rejects duplicate explicit project options" {
run env \
BASE_HOME="$BASE_REPO_ROOT" \
Expand Down
2 changes: 2 additions & 0 deletions cli/bash/commands/basectl/tests/runtime-dispatch.bats
Original file line number Diff line number Diff line change
Expand Up @@ -470,13 +470,15 @@ load ./basectl_helpers.bash
bash -c '
source "$BASE_HOME/cli/bash/commands/basectl/basectl.sh"
printf "label=%s\n" "$(basectl_run_bundle_label setup base-demo)"
printf "explicit-label=%s\n" "$(basectl_run_bundle_label activate --project help)"
BASE_CACHE_DIR="$BASE_TEST_TMPDIR/cache" basectl_initialize_run_bundle setup base-demo || exit $?
printf "run_id=%s\n" "$BASE_CLI_RUN_ID"
printf "run_root=%s\n" "$BASE_CLI_RUN_ROOT"
'

[ "$status" -eq 0 ]
[[ "$output" == *"label=setup__base-demo"* ]]
[[ "$output" == *"explicit-label=activate__help"* ]]
[[ "$output" == *"run_root="*"__setup__base-demo" ]]
run_id="$(printf '%s\n' "$output" | sed -n 's/^run_id=//p')"
[[ "$run_id" != *"__"* ]]
Expand Down
Loading
Loading