diff --git a/.ai-context/COMMANDS.md b/.ai-context/COMMANDS.md index f939e294..a0fb71fb 100644 --- a/.ai-context/COMMANDS.md +++ b/.ai-context/COMMANDS.md @@ -132,7 +132,8 @@ Important Python packages include: - `base_logs` - runtime log inspection. - `base_clean` - runtime cache/log/temp cleanup. - `base_release` - release check/plan/notes/publish support. -- `base_dev` - developer profile setup/check/doctor/onboard support. +- `base_dev` - prerequisite profile setup/check/doctor/onboard support, + including dev, sre, ai, and linux-lab. - `base_export_context` - deterministic local Markdown and Zip exports from a project's `.ai-context/` directory. Provider uploads are intentionally out of scope. diff --git a/.ai-context/DECISIONS.md b/.ai-context/DECISIONS.md index f9ee9610..47012ec6 100644 --- a/.ai-context/DECISIONS.md +++ b/.ai-context/DECISIONS.md @@ -60,6 +60,9 @@ architecture discussion. hardcoded inside `basectl build`. - New tool families such as AI developer tools should stay explicit and opt-in. +- Host VM tooling should stay explicit and opt-in. The `linux-lab` profile may + install and check Multipass on macOS hosts, but Base setup should not create + or mutate VM instances automatically. - AI agent harnesses such as Codex CLI, Claude Code, Cursor agents, Omnigent, and Pi-like tools are optional adjacent tools. Base may help install or check explicit AI prerequisites, export portable context, render repo-owned prompts, diff --git a/.ai-context/STATUS.md b/.ai-context/STATUS.md index 95f0245b..5cab3c90 100644 --- a/.ai-context/STATUS.md +++ b/.ai-context/STATUS.md @@ -32,6 +32,8 @@ The current command surface covers: - repo-owned prompt rendering through `basectl prompt` - documentation entrypoint opening through `basectl docs` - explicit `ai` prerequisite profile for Codex CLI and Claude Code +- explicit `linux-lab` prerequisite profile for host-side Multipass checks and + setup - Ubuntu/Debian runtime checks, diagnostics, and source-checkout validation ## Active Development Direction @@ -69,6 +71,8 @@ Recent released work includes: - newcomer orientation presentation docs - optional project Git remote reachability diagnostics - explicit `ai` prerequisite profile +- explicit host-scoped `linux-lab` prerequisite profile for Multipass checks + and setup - portable project Git workflow guidance from `basectl repo init` - Homebrew upgrade path preservation for explicit `BASE_HOME` and shell startup snippets diff --git a/CHANGELOG.md b/CHANGELOG.md index 01efc9ee..11f7da41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,17 @@ and Base versions are tracked in the repo-root `VERSION` file. ## [Unreleased] +### Added + +- Added an explicit `linux-lab` prerequisite profile that checks and installs + Multipass via Homebrew cask for local Ubuntu lab VMs without creating VM + instances during Base setup. + ### Fixed +- Replaced stray shell `run` calls in setup's virtualenv and apt install paths + with direct command execution so spawned `basectl setup` runs do not depend on + a test-harness helper. - Stopped default project checks from failing Ubuntu/Linux acceptance solely because manifest-declared IDE extension CLIs such as `code` are not on `PATH`; IDE extension diagnostics now run with the developer profile. diff --git a/FAQ.md b/FAQ.md index b10f5e24..d680ac91 100644 --- a/FAQ.md +++ b/FAQ.md @@ -248,10 +248,12 @@ to operate. ### Why can setup fail on a Homebrew `brew link` conflict? -Prerequisite profiles install ordinary Homebrew tools. A tool can pull a -Homebrew dependency that needs to be linked into Homebrew's prefix. If files such -as `/usr/local/bin/python3`, `/usr/local/bin/pip3`, or `/usr/local/bin/idle3` -already point at another Python installation, Homebrew may stop with: +Prerequisite profiles can install ordinary Homebrew tools and, for explicitly +selected host profiles, Homebrew casks such as Multipass. A tool can pull a +Homebrew dependency that needs to be linked into Homebrew's prefix. If files +such as `/usr/local/bin/python3`, `/usr/local/bin/pip3`, or +`/usr/local/bin/idle3` already point at another Python installation, Homebrew +may stop with: ```text Error: The `brew link` step did not complete successfully diff --git a/README.md b/README.md index 5e947637..7b5f6f9e 100644 --- a/README.md +++ b/README.md @@ -1055,24 +1055,31 @@ installs Base bootstrap Python packages into that environment. For project artifact setup, Base first seeds the target project venv with `bootstrap: true` default artifacts and then invokes the Python project setup layer through `base-wrapper --project `. -Prerequisite profiles are opt-in and manifest-driven. Use `--profile dev` to -install Base contributor tools such as BATS, the GitHub CLI, and ShellCheck from +Prerequisite profiles are opt-in. Use `--profile dev` to install Base +contributor tools such as BATS, the GitHub CLI, and ShellCheck from `lib/base/dev_manifest.yaml`. Use `--profile sre` for the initial site-reliability profile in `lib/base/sre_manifest.yaml`, which installs local diagnostic tools such as `kubectl`, `helm`, `k9s`, `httpie`, `grpcurl`, `jq`, `yq`, `nmap`, and `mtr`. Use `--profile ai` for optional AI coding tools: -Codex CLI and Claude Code. Profiles compose with a comma-separated list. +Codex CLI and Claude Code. Use `--profile linux-lab` on a macOS host to install +and check Multipass for local Ubuntu lab VMs. Profiles compose with a +comma-separated list. ```bash basectl setup --profile dev basectl setup --profile sre basectl setup --profile ai +basectl setup --profile linux-lab --dry-run +basectl setup --profile linux-lab basectl setup --profile dev,sre basectl setup --profile dev,ai +basectl setup --profile dev,linux-lab basectl check --profile sre basectl check --profile ai +basectl check --profile linux-lab basectl doctor --profile sre basectl doctor --profile ai +basectl doctor --profile linux-lab ``` AI coding tools are intentionally not part of the plain `dev` or `sre` profile. @@ -1081,6 +1088,12 @@ profile is explicitly requested. Base checks tool presence and version output, but it does not manage accounts, credentials, model access, or organization policy. +The `linux-lab` profile is intentionally host-scoped. It installs or checks the +Multipass CLI on macOS through `brew install --cask multipass`, but Base does +not create, start, mount, or delete Multipass instances during setup. Review +the planned install with `--dry-run`, then create lab VMs with +`multipass launch` when you are ready. + For the allowed Homebrew, Codex CLI, and Claude Code installer URLs, dry-run behavior, non-interactive behavior, and managed-device guidance, see [Remote Installer Policy](docs/remote-installer-policy.md). diff --git a/cli/bash/commands/basectl/subcommands/check.sh b/cli/bash/commands/basectl/subcommands/check.sh index f41f92f1..9450f4bb 100644 --- a/cli/bash/commands/basectl/subcommands/check.sh +++ b/cli/bash/commands/basectl/subcommands/check.sh @@ -14,7 +14,7 @@ Usage: basectl check [project] [options] Options: - --profile Include named prerequisite profiles. Known profiles: dev, sre, ai. + --profile Include named prerequisite profiles. Known profiles: dev, sre, ai, linux-lab. --format Select output format. Defaults to text. --manifest Use a specific base_manifest.yaml path for project checks. --remote-network Opt in to bounded project Git origin reachability checks. @@ -23,9 +23,10 @@ Options: Profiles: Profile lists are comma-separated, for example: --profile dev,sre. - dev - Base development tooling for this repository. - sre - production/SRE prerequisite tooling. - ai - AI coding assistant tooling. + dev - Base development tooling for this repository. + sre - production/SRE prerequisite tooling. + ai - AI coding assistant tooling. + linux-lab - Multipass tooling for local Ubuntu lab VMs on macOS hosts. Purpose: Verify the local Base CLI environment and, when provided, project artifacts on supported platforms without making changes. diff --git a/cli/bash/commands/basectl/subcommands/ci.sh b/cli/bash/commands/basectl/subcommands/ci.sh index a58ef644..43217642 100644 --- a/cli/bash/commands/basectl/subcommands/ci.sh +++ b/cli/bash/commands/basectl/subcommands/ci.sh @@ -19,16 +19,17 @@ Usage: Options: --format Select output format. Defaults to text. --manifest Use a specific base_manifest.yaml path. - --profile Include named prerequisite profiles. Known profiles: dev, sre, ai. + --profile Include named prerequisite profiles. Known profiles: dev, sre, ai, linux-lab. --recreate-venv Back up and recreate the project virtual environment during setup. -v Enable DEBUG logging for this subcommand. -h, --help Show this help text. Profiles: Profile lists are comma-separated, for example: --profile dev,sre. - dev - Base development tooling for this repository. - sre - production/SRE prerequisite tooling. - ai - AI coding assistant tooling. + dev - Base development tooling for this repository. + sre - production/SRE prerequisite tooling. + ai - AI coding assistant tooling. + linux-lab - Multipass tooling for local Ubuntu lab VMs on macOS hosts. Purpose: Run Base setup, checks, and diagnostics in a non-interactive CI environment. diff --git a/cli/bash/commands/basectl/subcommands/doctor.sh b/cli/bash/commands/basectl/subcommands/doctor.sh index 32e122cc..f831b21d 100644 --- a/cli/bash/commands/basectl/subcommands/doctor.sh +++ b/cli/bash/commands/basectl/subcommands/doctor.sh @@ -14,7 +14,7 @@ Usage: basectl doctor [project] [options] Options: - --profile Include named prerequisite profiles. Known profiles: dev, sre, ai. + --profile Include named prerequisite profiles. Known profiles: dev, sre, ai, linux-lab. --format Select output format. Defaults to text. --manifest Use a specific base_manifest.yaml path for project diagnostics. --remote-network Opt in to bounded project Git origin reachability diagnostics. @@ -24,9 +24,10 @@ Options: Profiles: Profile lists are comma-separated, for example: --profile dev,sre. - dev - Base development tooling for this repository. - sre - production/SRE prerequisite tooling. - ai - AI coding assistant tooling. + dev - Base development tooling for this repository. + sre - production/SRE prerequisite tooling. + ai - AI coding assistant tooling. + linux-lab - Multipass tooling for local Ubuntu lab VMs on macOS hosts. Purpose: Diagnose the local Base CLI environment and, when provided, project artifacts. diff --git a/cli/bash/commands/basectl/subcommands/onboard.sh b/cli/bash/commands/basectl/subcommands/onboard.sh index 69197323..020ab2ad 100644 --- a/cli/bash/commands/basectl/subcommands/onboard.sh +++ b/cli/bash/commands/basectl/subcommands/onboard.sh @@ -14,7 +14,7 @@ Usage: basectl onboard [project] [options] Options: - --profile Include named prerequisite profiles. Known profiles: dev, sre, ai. + --profile Include named prerequisite profiles. Known profiles: dev, sre, ai, linux-lab. --dry-run Explain planned onboarding steps without making changes. --yes Accept default answers for setup and shell profile prompts. --no-profile Skip shell profile updates. diff --git a/cli/bash/commands/basectl/subcommands/setup.sh b/cli/bash/commands/basectl/subcommands/setup.sh index eaf544bd..12b147aa 100644 --- a/cli/bash/commands/basectl/subcommands/setup.sh +++ b/cli/bash/commands/basectl/subcommands/setup.sh @@ -14,7 +14,7 @@ Usage: basectl setup [options] [project] Options: - --profile Install named prerequisite profiles. Known profiles: dev, sre, ai. + --profile Install named prerequisite profiles. Known profiles: dev, sre, ai, linux-lab. --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. @@ -26,9 +26,10 @@ Options: Profiles: Profile lists are comma-separated, for example: --profile dev,sre. - dev - Base development tooling for this repository. - sre - production/SRE prerequisite tooling. - ai - AI coding assistant tooling. + dev - Base development tooling for this repository. + sre - production/SRE prerequisite tooling. + ai - AI coding assistant tooling. + linux-lab - Multipass tooling for local Ubuntu lab VMs on macOS hosts. Purpose: Prepare the local Base CLI environment on supported setup platforms. diff --git a/cli/bash/commands/basectl/subcommands/setup_common.sh b/cli/bash/commands/basectl/subcommands/setup_common.sh index 4adf2a27..580123bd 100644 --- a/cli/bash/commands/basectl/subcommands/setup_common.sh +++ b/cli/bash/commands/basectl/subcommands/setup_common.sh @@ -68,11 +68,11 @@ setup_enable_debug_logging() { } setup_supported_profiles() { - printf '%s\n' "dev sre ai" + printf '%s\n' "dev sre ai linux-lab" } setup_supported_profiles_display() { - printf '%s\n' "dev, sre, ai" + printf '%s\n' "dev, sre, ai, linux-lab" } setup_epoch_seconds() { @@ -87,7 +87,7 @@ setup_profile_supported() { local profile="$1" case "$profile" in - dev|sre|ai) + dev|sre|ai|linux-lab) return 0 ;; *) @@ -326,7 +326,7 @@ setup_backup_existing_venv_path() { fi log_info "Moving $description '$venv_dir' to '$backup_path'." - run mv "$venv_dir" "$backup_path" + mv "$venv_dir" "$backup_path" || fatal_error "Unable to move $description '$venv_dir' to '$backup_path'." } setup_python_formula() { @@ -727,7 +727,12 @@ setup_install_homebrew() { if [[ -n "${BASE_SETUP_HOMEBREW_INSTALLER_SCRIPT:-}" ]]; then setup_reject_test_hook_if_disallowed BASE_SETUP_HOMEBREW_INSTALLER_SCRIPT - run "$BASE_SETUP_HOMEBREW_INSTALLER_SCRIPT" + "$BASE_SETUP_HOMEBREW_INSTALLER_SCRIPT" + exit_code=$? + if ((exit_code)); then + log_error "$(setup_recovery_homebrew)" + fi + exit_if_error "$exit_code" "Homebrew installation failed." else command -v curl >/dev/null 2>&1 || fatal_error "curl is required to install Homebrew. Install curl or install Homebrew manually from https://brew.sh/, then rerun 'basectl setup'." /bin/bash -c "$(curl -fsSL "$installer_url")" @@ -772,7 +777,7 @@ setup_install_xcode_tools() { fi log_info "Installing Xcode Command Line Tools." - run --no-exit xcode-select --install + xcode-select --install || true timeout="$(setup_xcode_wait_timeout_seconds)" interval="$(setup_xcode_wait_interval_seconds)" @@ -815,7 +820,7 @@ setup_install_python() { brew_bin="$(setup_find_brew_bin)" || fatal_error "Homebrew is required to install Python formula '$formula'. $(setup_recovery_homebrew)" log_info "Installing Python formula '$formula' via Homebrew." - run "$brew_bin" install "$formula" + "$brew_bin" install "$formula" || fatal_error "Homebrew failed to install Python formula '$formula'." } setup_find_python_bin() { @@ -974,7 +979,7 @@ setup_create_virtualenv() { safe_mkdir -p "$(dirname "$venv_dir")" log_info "Creating Python virtual environment at '$venv_dir'." - run "$python_bin" -m venv "$venv_dir" + "$python_bin" -m venv "$venv_dir" } setup_base_venv_python_bin() { @@ -1019,7 +1024,8 @@ setup_install_base_python_package() { python_bin="$(setup_base_venv_python_bin "$venv_dir")" || fatal_error "Base virtual environment Python was not found at '$venv_dir/bin/python'. $(setup_recovery_venv)" log_info "Installing Python package '$package' in the Base virtual environment." - run "$python_bin" -m pip install --disable-pip-version-check "$package" + "$python_bin" -m pip install --disable-pip-version-check "$package" || + fatal_error "Unable to install Python package '$package' in the Base virtual environment." } setup_install_pyyaml() { @@ -2649,8 +2655,8 @@ setup_run_linux_debian_apt_prerequisites() { fi log_info "Installing Ubuntu/Debian apt prerequisites." - run sudo apt-get update || return $? - run sudo apt-get install -y "${package_args[@]}" + sudo apt-get update || return $? + sudo apt-get install -y "${package_args[@]}" } setup_run_linux_debian_install() { diff --git a/cli/bash/commands/basectl/tests/check.bats b/cli/bash/commands/basectl/tests/check.bats index 88e97539..5a67b66e 100644 --- a/cli/bash/commands/basectl/tests/check.bats +++ b/cli/bash/commands/basectl/tests/check.bats @@ -12,9 +12,10 @@ load ./setup_helpers.bash [[ "$output" != *"--dev"* ]] [[ "$output" == *"--profile "* ]] [[ "$output" == *"Profile lists are comma-separated, for example: --profile dev,sre."* ]] - [[ "$output" == *"dev - Base development tooling for this repository."* ]] - [[ "$output" == *"sre - production/SRE prerequisite tooling."* ]] - [[ "$output" == *"ai - AI coding assistant tooling."* ]] + [[ "$output" == *"dev - Base development tooling for this repository."* ]] + [[ "$output" == *"sre - production/SRE prerequisite tooling."* ]] + [[ "$output" == *"ai - AI coding assistant tooling."* ]] + [[ "$output" == *"linux-lab - Multipass tooling for local Ubuntu lab VMs on macOS hosts."* ]] [[ "$output" == *"--remote-network"* ]] [[ "$output" == *"Verify the local Base CLI environment and, when provided, project artifacts on supported platforms without making changes."* ]] [[ "$output" == *"Use check for a quick pass/fail result; use doctor for finding IDs and fix hints."* ]] @@ -302,10 +303,10 @@ EOF touch "$TEST_STATE_DIR/click-installed" create_base_venv_stub "$venv_dir" - run_base_command check --profile dev,SRE,AI + run_base_command check --profile dev,SRE,AI,LINUX-LAB [ "$status" -eq 1 ] - [ "$(cat "$TEST_STATE_DIR/dev-args")" = "$(printf '%s\n' check --profile dev,sre,ai)" ] + [ "$(cat "$TEST_STATE_DIR/dev-args")" = "$(printf '%s\n' check --profile dev,sre,ai,linux-lab)" ] [[ "$output" == *"Base CLI environment check found missing requirements."* ]] } @@ -313,7 +314,7 @@ EOF run_base_command check --profile ops [ "$status" -eq 2 ] - [[ "$output" == *"Unsupported profile 'ops'. Expected one of: dev, sre, ai."* ]] + [[ "$output" == *"Unsupported profile 'ops'. Expected one of: dev, sre, ai, linux-lab."* ]] } @test "basectl check rejects empty profile list entries" { diff --git a/cli/bash/commands/basectl/tests/ci.bats b/cli/bash/commands/basectl/tests/ci.bats index 3b90595a..7e96766b 100644 --- a/cli/bash/commands/basectl/tests/ci.bats +++ b/cli/bash/commands/basectl/tests/ci.bats @@ -42,9 +42,10 @@ prepare_ci_runtime() { [[ "$output" == *"--format "* ]] [[ "$output" == *"--profile "* ]] [[ "$output" == *"Profile lists are comma-separated, for example: --profile dev,sre."* ]] - [[ "$output" == *"dev - Base development tooling for this repository."* ]] - [[ "$output" == *"sre - production/SRE prerequisite tooling."* ]] - [[ "$output" == *"ai - AI coding assistant tooling."* ]] + [[ "$output" == *"dev - Base development tooling for this repository."* ]] + [[ "$output" == *"sre - production/SRE prerequisite tooling."* ]] + [[ "$output" == *"ai - AI coding assistant tooling."* ]] + [[ "$output" == *"linux-lab - Multipass tooling for local Ubuntu lab VMs on macOS hosts."* ]] [[ "$output" == *"BASE_CI=true"* ]] } diff --git a/cli/bash/commands/basectl/tests/completions.bats b/cli/bash/commands/basectl/tests/completions.bats index 35384794..d2914953 100644 --- a/cli/bash/commands/basectl/tests/completions.bats +++ b/cli/bash/commands/basectl/tests/completions.bats @@ -229,7 +229,7 @@ EOF [[ "$output" == *"update_projects=base demo"* ]] [[ "$output" == *"check_options=--profile --format --manifest --remote-network"* ]] [[ "$output" == *"update_options=--dry-run"* ]] - [[ "$output" == *"check_profiles=dev sre ai dev,sre dev,ai sre,ai dev,sre,ai"* ]] + [[ "$output" == *"check_profiles=dev sre ai linux-lab dev,sre dev,ai dev,linux-lab sre,ai sre,linux-lab ai,linux-lab dev,sre,ai dev,sre,linux-lab dev,ai,linux-lab sre,ai,linux-lab dev,sre,ai,linux-lab"* ]] [[ "$output" == *"test_options=--workspace --dry-run"* ]] [[ "$output" == *"build_options=--workspace --dry-run --list"* ]] [[ "$output" == *"run_options=--workspace --dry-run --list"* ]] @@ -243,7 +243,7 @@ EOF [[ "$output" == *"workspace_configure_options=--workspace --manifest --dry-run"* ]] [[ "$output" == *"onboard_options=--profile --dry-run --yes --no-profile"* ]] [[ "$output" == *"onboard_projects=base demo"* ]] - [[ "$output" == *"onboard_profiles=dev sre ai dev,sre dev,ai sre,ai dev,sre,ai"* ]] + [[ "$output" == *"onboard_profiles=dev sre ai linux-lab dev,sre dev,ai dev,linux-lab sre,ai sre,linux-lab ai,linux-lab dev,sre,ai dev,sre,linux-lab dev,ai,linux-lab sre,ai,linux-lab dev,sre,ai,linux-lab"* ]] [[ "$output" == *"prompt_names=list product-self-review"* ]] [[ "$output" == *"prompt_options=--help"* ]] [[ "$output" == *"docs_options=--show-url"* ]] diff --git a/cli/bash/commands/basectl/tests/doctor.bats b/cli/bash/commands/basectl/tests/doctor.bats index 41729108..e9151ba7 100644 --- a/cli/bash/commands/basectl/tests/doctor.bats +++ b/cli/bash/commands/basectl/tests/doctor.bats @@ -176,9 +176,10 @@ EOF [[ "$output" == *"basectl doctor [project] [options]"* ]] [[ "$output" == *"--profile "* ]] [[ "$output" == *"Profile lists are comma-separated, for example: --profile dev,sre."* ]] - [[ "$output" == *"dev - Base development tooling for this repository."* ]] - [[ "$output" == *"sre - production/SRE prerequisite tooling."* ]] - [[ "$output" == *"ai - AI coding assistant tooling."* ]] + [[ "$output" == *"dev - Base development tooling for this repository."* ]] + [[ "$output" == *"sre - production/SRE prerequisite tooling."* ]] + [[ "$output" == *"ai - AI coding assistant tooling."* ]] + [[ "$output" == *"linux-lab - Multipass tooling for local Ubuntu lab VMs on macOS hosts."* ]] [[ "$output" == *"--remote-network"* ]] [[ "$output" == *"--no-color"* ]] [[ "$output" != *"--dev"* ]] @@ -582,7 +583,7 @@ EOF run_basectl doctor --profile ops [ "$status" -eq 2 ] - [[ "$output" == *"Unsupported profile 'ops'. Expected one of: dev, sre, ai."* ]] + [[ "$output" == *"Unsupported profile 'ops'. Expected one of: dev, sre, ai, linux-lab."* ]] } @test "basectl doctor reports errors with suggested fixes" { diff --git a/cli/bash/commands/basectl/tests/onboard.bats b/cli/bash/commands/basectl/tests/onboard.bats index 05b7963b..b7e3980c 100644 --- a/cli/bash/commands/basectl/tests/onboard.bats +++ b/cli/bash/commands/basectl/tests/onboard.bats @@ -102,7 +102,7 @@ load ./basectl_helpers.bash ' [ "$status" -eq 2 ] - [[ "$output" == *"Unsupported profile 'ops'. Expected one of: dev, sre, ai."* ]] + [[ "$output" == *"Unsupported profile 'ops'. Expected one of: dev, sre, ai, linux-lab."* ]] } @test "basectl onboard declines setup conservatively" { diff --git a/cli/bash/commands/basectl/tests/setup.bats b/cli/bash/commands/basectl/tests/setup.bats index edff3f4e..61c1f0a7 100644 --- a/cli/bash/commands/basectl/tests/setup.bats +++ b/cli/bash/commands/basectl/tests/setup.bats @@ -12,9 +12,10 @@ load ./setup_helpers.bash [[ "$output" != *"--dev"* ]] [[ "$output" == *"--profile "* ]] [[ "$output" == *"Profile lists are comma-separated, for example: --profile dev,sre."* ]] - [[ "$output" == *"dev - Base development tooling for this repository."* ]] - [[ "$output" == *"sre - production/SRE prerequisite tooling."* ]] - [[ "$output" == *"ai - AI coding assistant tooling."* ]] + [[ "$output" == *"dev - Base development tooling for this repository."* ]] + [[ "$output" == *"sre - production/SRE prerequisite tooling."* ]] + [[ "$output" == *"ai - AI coding assistant tooling."* ]] + [[ "$output" == *"linux-lab - Multipass tooling for local Ubuntu lab VMs on macOS hosts."* ]] [[ "$output" == *"--notify"* ]] [[ "$output" == *"--no-notify"* ]] [[ "$output" == *"--recreate-venv"* ]] @@ -882,27 +883,47 @@ EOF [ "$(cat "$TEST_STATE_DIR/dev-args")" = "$(printf '%s\n' setup --profile ai)" ] } +@test "basectl setup --profile linux-lab runs the Python prerequisite profile layer" { + local venv_dir="$TEST_HOME/.base.d/base/.venv" + + create_linux_dpkg_query_stub + create_system_python3_stub + touch "$TEST_STATE_DIR/pyyaml-installed" + touch "$TEST_STATE_DIR/click-installed" + create_base_venv_stub "$venv_dir" + + run_base_command \ + BASE_SETUP_TEST_PLATFORM=linux-debian \ + setup --profile linux-lab + + [ "$status" -eq 0 ] + [ -f "$TEST_STATE_DIR/dev-setup-ran" ] + [ "$(cat "$TEST_STATE_DIR/dev-args")" = "$(printf '%s\n' setup --profile linux-lab)" ] +} + @test "basectl setup accepts comma separated profile lists case-insensitively" { - local installer + local venv_dir="$TEST_HOME/.base.d/base/.venv" - create_xcode_stubs - installer="$(create_homebrew_installer_stub)" + create_linux_dpkg_query_stub + create_system_python3_stub + touch "$TEST_STATE_DIR/pyyaml-installed" + touch "$TEST_STATE_DIR/click-installed" + create_base_venv_stub "$venv_dir" run_base_command \ - BASE_SETUP_ALLOW_NONINTERACTIVE_XCODE_INSTALL=true \ - BASE_SETUP_HOMEBREW_INSTALLER_SCRIPT="$installer" \ - setup --profile dev,SRE,AI + BASE_SETUP_TEST_PLATFORM=linux-debian \ + setup --profile dev,SRE,AI,LINUX-LAB [ "$status" -eq 0 ] [ -f "$TEST_STATE_DIR/dev-setup-ran" ] - [ "$(cat "$TEST_STATE_DIR/dev-args")" = "$(printf '%s\n' setup --profile dev,sre,ai)" ] + [ "$(cat "$TEST_STATE_DIR/dev-args")" = "$(printf '%s\n' setup --profile dev,sre,ai,linux-lab)" ] } @test "basectl setup rejects unknown profiles" { run_base_command setup --profile ops [ "$status" -eq 2 ] - [[ "$output" == *"Unsupported profile 'ops'. Expected one of: dev, sre, ai."* ]] + [[ "$output" == *"Unsupported profile 'ops'. Expected one of: dev, sre, ai, linux-lab."* ]] } @test "basectl setup rejects empty profile list entries" { diff --git a/cli/bash/commands/basectl/tests/setup_helpers.bash b/cli/bash/commands/basectl/tests/setup_helpers.bash index 22d85e08..55f94cd0 100644 --- a/cli/bash/commands/basectl/tests/setup_helpers.bash +++ b/cli/bash/commands/basectl/tests/setup_helpers.bash @@ -678,10 +678,22 @@ if [[ "${1:-}" == "-m" && "${2:-}" == "pip" && "${3:-}" == "show" && "${4:-}" == [[ -f "${BASE_SETUP_TEST_STATE_DIR:?}/click-installed" ]] exit $? fi +if [[ "${1:-}" == "-m" && "${2:-}" == "base_setup" ]]; then + if [[ "$*" == *"--action route"* ]]; then + printf 'base\t%s\t%s\t%s\tfalse\n' "$BASE_HOME" "$BASE_HOME/base_manifest.yaml" "$HOME/.base.d/base/.venv" + exit 0 + fi + touch "${BASE_SETUP_TEST_STATE_DIR:?}/project-setup-ran" + exit 0 +fi if [[ "${1:-}" == "-m" && "${2:-}" == "base_dev" ]]; then shift 2 printf '%s\n' "$@" > "${BASE_SETUP_TEST_STATE_DIR:?}/dev-args" case "${1:-}" in + setup) + touch "${BASE_SETUP_TEST_STATE_DIR:?}/dev-setup-ran" + exit 0 + ;; check) if [[ "${2:-}" == "--format" && "${3:-}" == "json" ]]; then printf '{"schema_version":1,"status":"error","profiles":["dev"],"checks":[{"id":"BASE-D104","status":"error","name":"bats-core","message":"Artifact '\''bats-core'\'' is not installed via Homebrew package '\''bats-core'\''.","fix":"basectl setup --profile dev"},{"id":"BASE-D104","status":"error","name":"gh","message":"Artifact '\''gh'\'' is not installed via Homebrew package '\''gh'\''.","fix":"basectl setup --profile dev"}]}\n' diff --git a/cli/python/base_dev/engine.py b/cli/python/base_dev/engine.py index d1c7f8b5..c2d64f42 100644 --- a/cli/python/base_dev/engine.py +++ b/cli/python/base_dev/engine.py @@ -23,10 +23,12 @@ from .checks import checks_status from .checks import doctor_status from .checks import print_doctor_finding +from .linux_lab import linux_lab_checks +from .linux_lab import setup_linux_lab app = base_cli.App(name="base_dev") -SUPPORTED_PROFILES = ("dev", "sre", "ai") +SUPPORTED_PROFILES = ("dev", "sre", "ai", "linux-lab") @dataclass(frozen=True) @@ -118,7 +120,7 @@ def normalize_profiles(profiles: tuple[str, ...]) -> tuple[str, ...]: def read_profile_manifests(ctx: base_cli.Context, profiles: tuple[str, ...]) -> tuple[ProfileManifest, ...]: profile_manifests: list[ProfileManifest] = [] for profile in profiles: - if profile == "ai": + if profile in {"ai", "linux-lab"}: continue manifest = read_profile_manifest(ctx, profile) definitions = resolve_artifact_definitions(manifest.artifacts) @@ -178,6 +180,8 @@ def setup_profiles( for profile in profiles: if profile == "ai": status = setup_ai_tools(ctx, dry_run=dry_run) + elif profile == "linux-lab": + status = setup_linux_lab(ctx, dry_run=dry_run) else: profile_manifest = profile_manifest_by_name[profile] status = setup_profile_artifacts( @@ -376,6 +380,9 @@ def collect_profile_checks( if profile == "ai": checks.extend(ai_tool_checks()) continue + if profile == "linux-lab": + checks.extend(linux_lab_checks()) + continue profile_manifest = profile_manifest_by_name[profile] checks.extend( dev_checks( diff --git a/cli/python/base_dev/linux_lab.py b/cli/python/base_dev/linux_lab.py new file mode 100644 index 00000000..9699ea4a --- /dev/null +++ b/cli/python/base_dev/linux_lab.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import shutil +import subprocess + +import base_cli +from base_setup import process +from base_setup.errors import ArtifactError +from base_setup.platform_policy import current_base_platform +from base_setup.platform_policy import platform_label +from base_setup.process import DIAGNOSTIC_TIMEOUT_SECONDS + +from .checks import DevCheck + + +MULTIPASS_INSTALL_COMMAND = ("brew", "install", "--cask", "multipass") +LINUX_LAB_FINDING_ID = "BASE-D108" + + +def setup_linux_lab(ctx: base_cli.Context, dry_run: bool) -> int: + ctx.log.info("Setting up Base 'linux-lab' prerequisites.") + check = check_multipass() + if check.ok: + ctx.log.info("%s", check.message) + ctx.log.info("Base 'linux-lab' prerequisite setup is complete.") + return base_cli.ExitCode.SUCCESS + + ctx.log.info( + "Multipass creates host-managed Ubuntu VMs; Base does not create VM instances during setup." + ) + if current_base_platform() not in {"", "macos"}: + ctx.log.error( + "The 'linux-lab' setup profile installs Multipass via Homebrew cask and is supported " + "only on macOS hosts. Current BASE_PLATFORM='%s'. Install Multipass manually from " + "https://canonical.com/multipass/install if this host should manage lab VMs.", + platform_label(), + ) + return base_cli.ExitCode.FAILURE + try: + if dry_run: + process.dry_run_command(ctx, list(MULTIPASS_INSTALL_COMMAND)) + ctx.log.info("Base 'linux-lab' prerequisite setup dry-run is complete.") + return base_cli.ExitCode.SUCCESS + if not process.command_exists("brew"): + raise ArtifactError( + "Homebrew is required to install Multipass for the 'linux-lab' profile. " + "Install Multipass from https://canonical.com/multipass/install or run " + "'brew install --cask multipass' after Homebrew is available." + ) + ctx.log.info("Installing Multipass via Homebrew cask.") + process.run_command(ctx, list(MULTIPASS_INSTALL_COMMAND)) + except ArtifactError as exc: + ctx.log.error(str(exc)) + return base_cli.ExitCode.FAILURE + + ctx.log.info("Base 'linux-lab' prerequisite setup is complete.") + return base_cli.ExitCode.SUCCESS + + +def linux_lab_checks() -> tuple[DevCheck, ...]: + return (check_multipass(),) + + +def check_multipass() -> DevCheck: + executable_path = shutil.which("multipass") + if executable_path is None: + return DevCheck( + name="multipass", + ok=False, + message="Multipass 'multipass' was not found.", + fix="basectl setup --profile linux-lab", + finding_id=LINUX_LAB_FINDING_ID, + ) + + command = [executable_path, "version"] + try: + completed = subprocess.run( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=False, + timeout=DIAGNOSTIC_TIMEOUT_SECONDS, + ) + except subprocess.TimeoutExpired: + return DevCheck( + name="multipass", + ok=False, + message=f"Multipass version check timed out after {DIAGNOSTIC_TIMEOUT_SECONDS} seconds.", + fix="Retry 'multipass version' or run 'basectl setup --profile linux-lab'.", + status="warn", + finding_id=LINUX_LAB_FINDING_ID, + ) + + if completed.returncode != 0: + detail = summarize_command_output(completed.stderr) or summarize_command_output(completed.stdout) + message = f"Multipass version check failed with exit {completed.returncode}." + if detail: + message = f"{message} {detail}" + return DevCheck( + name="multipass", + ok=False, + message=message, + fix="basectl setup --profile linux-lab", + finding_id=LINUX_LAB_FINDING_ID, + ) + + version = ( + summarize_command_output(completed.stdout) + or summarize_command_output(completed.stderr) + or "version unknown" + ) + return DevCheck( + name="multipass", + ok=True, + message=f"Multipass is already installed at '{executable_path}' ({version}).", + fix="", + finding_id=LINUX_LAB_FINDING_ID, + ) + + +def summarize_command_output(output: str | None) -> str: + return " ".join((output or "").split()) diff --git a/cli/python/base_dev/tests/test_engine.py b/cli/python/base_dev/tests/test_engine.py index af106a37..4d11f207 100644 --- a/cli/python/base_dev/tests/test_engine.py +++ b/cli/python/base_dev/tests/test_engine.py @@ -96,6 +96,10 @@ def test_normalize_profiles_defaults_to_dev_and_deduplicates(self) -> None: self.assertEqual(engine.normalize_profiles(()), ("dev",)) self.assertEqual(engine.normalize_profiles(("dev", "sre", "dev")), ("dev", "sre")) self.assertEqual(engine.normalize_profiles(("dev,SRE,AI",)), ("dev", "sre", "ai")) + self.assertEqual( + engine.normalize_profiles(("dev,LINUX-LAB,ai",)), + ("dev", "linux-lab", "ai"), + ) def test_normalize_profiles_rejects_unknown_profile(self) -> None: with self.assertRaisesRegex(engine.ProfileError, "Unsupported profile 'ops'"): diff --git a/cli/python/base_dev/tests/test_linux_lab.py b/cli/python/base_dev/tests/test_linux_lab.py new file mode 100644 index 00000000..9ea27404 --- /dev/null +++ b/cli/python/base_dev/tests/test_linux_lab.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import io +import importlib.util +import json +import os +import tempfile +import unittest +from contextlib import redirect_stderr, redirect_stdout +from pathlib import Path +from unittest import mock + +from base_dev.engine import main + + +def run_engine(args: list[str], extra_env: dict[str, str] | None = None) -> tuple[int, str, str]: + stdout = io.StringIO() + stderr = io.StringIO() + with tempfile.TemporaryDirectory() as home_dir: + env = { + "HOME": home_dir, + "BASE_HOME": str(Path(__file__).resolve().parents[4]), + "BASE_PLATFORM": "", + } + if extra_env: + env.update(extra_env) + with mock.patch.dict(os.environ, env): + with redirect_stdout(stdout), redirect_stderr(stderr): + status = main(args) + return status, stdout.getvalue(), stderr.getvalue() + + +@unittest.skipUnless(importlib.util.find_spec("click"), "Click is not installed") +class LinuxLabProfileTests(unittest.TestCase): + def test_check_profile_linux_lab_reports_missing_multipass(self) -> None: + with tempfile.TemporaryDirectory() as bin_dir: + status, stdout, stderr = run_engine( + ["check", "--profile", "linux-lab", "--format", "json"], + extra_env={"PATH": bin_dir}, + ) + + payload = json.loads(stdout) + self.assertEqual(status, 1) + self.assertEqual(stderr, "") + self.assertEqual(payload["status"], "error") + self.assertEqual(payload["profiles"], ["linux-lab"]) + self.assertEqual( + payload["checks"], + [ + { + "id": "BASE-D108", + "status": "error", + "name": "multipass", + "message": "Multipass 'multipass' was not found.", + "fix": "basectl setup --profile linux-lab", + } + ], + ) + + def test_setup_profile_linux_lab_dry_run_prints_multipass_install_plan(self) -> None: + with tempfile.TemporaryDirectory() as bin_dir: + status, _stdout, stderr = run_engine( + ["setup", "--profile", "linux-lab", "--dry-run"], + extra_env={"BASE_PLATFORM": "macos", "PATH": bin_dir}, + ) + + self.assertEqual(status, 0) + self.assertIn("Setting up Base 'linux-lab' prerequisites.", stderr) + self.assertIn("[DRY-RUN] Would run: brew install --cask multipass", stderr) + self.assertIn("Base 'linux-lab' prerequisite setup dry-run is complete.", stderr) + self.assertIn( + "Multipass creates host-managed Ubuntu VMs; Base does not create VM instances during setup.", + stderr, + ) + + def test_setup_profile_linux_lab_missing_multipass_is_macos_homebrew_only(self) -> None: + with tempfile.TemporaryDirectory() as bin_dir: + status, _stdout, stderr = run_engine( + ["setup", "--profile", "linux-lab"], + extra_env={"BASE_PLATFORM": "linux-debian", "PATH": bin_dir}, + ) + + self.assertEqual(status, 1) + self.assertIn( + "The 'linux-lab' setup profile installs Multipass via Homebrew cask " + "and is supported only on macOS hosts.", + stderr, + ) + + def test_doctor_profile_linux_lab_json_uses_stable_finding_id(self) -> None: + with tempfile.TemporaryDirectory() as bin_dir: + status, stdout, stderr = run_engine( + ["doctor", "--profile", "linux-lab", "--format", "json"], + extra_env={"PATH": bin_dir}, + ) + + findings = json.loads(stdout) + self.assertEqual(status, 1) + self.assertEqual(stderr, "") + self.assertEqual(findings[0]["id"], "BASE-D108") + self.assertEqual(findings[0]["fix"], "basectl setup --profile linux-lab") + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/doctor-findings.md b/docs/doctor-findings.md index 8327ab6d..20481adb 100644 --- a/docs/doctor-findings.md +++ b/docs/doctor-findings.md @@ -102,6 +102,7 @@ Doctor commands use the same diagnostic item fields. The top-level | `BASE-D105` | GitHub CLI availability | | `BASE-D106` | GitHub CLI authentication status | | `BASE-D107` | AI developer tool availability and version status | +| `BASE-D108` | Multipass availability and version status for the `linux-lab` profile | ## Project Findings diff --git a/docs/linux-support.md b/docs/linux-support.md index dd239969..5ff1603a 100644 --- a/docs/linux-support.md +++ b/docs/linux-support.md @@ -216,6 +216,38 @@ machine. Apple Silicon Macs running Ubuntu in Parallels should follow the same ARM64 package archives used by Parallels when the configured apt repositories provide them. +## macOS Linux Lab + +Mac developers can use the optional `linux-lab` prerequisite profile to prepare +a local Ubuntu VM host without making Linux support part of the default +developer profile: + +```bash +basectl setup --profile linux-lab --dry-run +basectl setup --profile linux-lab +basectl check --profile linux-lab +basectl doctor --profile linux-lab +``` + +The profile checks the `multipass` CLI and installs Multipass on macOS through +the Homebrew cask path when setup is run without `--dry-run`. It does not create +or mutate VM instances. After Multipass is installed, create a lab instance +explicitly: + +```bash +multipass launch 24.04 \ + --name ubuntu-dev \ + --cpus 8 \ + --memory 16G \ + --disk 120G \ + --mount "$HOME/work:/home/ubuntu/work" +multipass shell ubuntu-dev +``` + +Apple Silicon Macs normally run ARM64 Ubuntu guests. That is useful for local +Linux preflight work, but hosted `ubuntu-latest` GitHub Actions runners remain +a separate validation target. + ## Shell Startup Linux shell startup differs from macOS: diff --git a/docs/technical-overview.md b/docs/technical-overview.md index 380889cb..f7903c38 100644 --- a/docs/technical-overview.md +++ b/docs/technical-overview.md @@ -149,7 +149,7 @@ and their own build systems. See [Setup Hooks Boundary](setup-hooks.md). | Command | What it does | |---|---| -| `basectl setup [project] [--profile dev\|sre\|ai]` | Install / reconcile prerequisites | +| `basectl setup [project] [--profile dev\|sre\|ai\|linux-lab]` | Install / reconcile prerequisites | | `basectl update-profile [--defaults]` | Wire shell startup files | | `basectl update [project] [--dry-run]` | Upgrade Base or a Base-managed project checkout | | `basectl onboard [project]` | Guided first-run checklist | @@ -204,13 +204,14 @@ and their own build systems. See [Setup Hooks Boundary](setup-hooks.md). |---|---| | `basectl ci setup\|check\|doctor [--format json]` | Non-interactive CI entry point | -**Prerequisite profiles** (compose with commas: `--profile dev,ai`): +**Prerequisite profiles** (compose with commas: `--profile dev,linux-lab`): | Profile | Installs | |---|---| | `dev` | BATS, GitHub CLI, ShellCheck | | `sre` | kubectl, helm, k9s, jq, yq, httpie, nmap, mtr | | `ai` | Codex CLI, Claude Code | +| `linux-lab` | Multipass for local Ubuntu lab VMs on macOS hosts | ## Installation Paths diff --git a/lib/base/README.md b/lib/base/README.md index fb5e9a8e..488b0794 100644 --- a/lib/base/README.md +++ b/lib/base/README.md @@ -27,6 +27,10 @@ defaults and opt-in profiles. Project repositories should declare their own tools, commands, tests, demos, activation hooks, and project-specific artifacts in their own `base_manifest.yaml`. +The `ai` and `linux-lab` prerequisite profiles are code-backed because they +manage special host tool policies rather than manifest-declared Homebrew +artifacts. + Keep schema and behavior details in the canonical documentation instead of duplicating them here: diff --git a/lib/shell/completions/basectl_completion.sh b/lib/shell/completions/basectl_completion.sh index 2e762ce0..bce8b00d 100644 --- a/lib/shell/completions/basectl_completion.sh +++ b/lib/shell/completions/basectl_completion.sh @@ -92,7 +92,7 @@ _base_basectl_completion_compgen() { } _base_basectl_completion_profiles() { - printf '%s\n' "dev sre ai dev,sre dev,ai sre,ai dev,sre,ai" + printf '%s\n' "dev sre ai linux-lab dev,sre dev,ai dev,linux-lab sre,ai sre,linux-lab ai,linux-lab dev,sre,ai dev,sre,linux-lab dev,ai,linux-lab sre,ai,linux-lab dev,sre,ai,linux-lab" } _base_basectl_completion_project_or_options() { diff --git a/lib/shell/completions/basectl_completion.zsh b/lib/shell/completions/basectl_completion.zsh index 94488b67..b50091f0 100644 --- a/lib/shell/completions/basectl_completion.zsh +++ b/lib/shell/completions/basectl_completion.zsh @@ -178,7 +178,7 @@ _base_basectl_completion() { esac ;; setup) - _arguments '--profile[Install prerequisite profiles]:profile:(dev sre ai dev,sre dev,ai sre,ai dev,sre,ai)' \ + _arguments '--profile[Install prerequisite profiles]:profile:(dev sre ai linux-lab dev,sre dev,ai dev,linux-lab sre,ai sre,linux-lab ai,linux-lab dev,sre,ai dev,sre,linux-lab dev,ai,linux-lab sre,ai,linux-lab dev,sre,ai,linux-lab)' \ '--dry-run[Log without making changes]' \ '--manifest[Use a specific manifest]:path:_files' \ '--notify[Force a setup completion notification]' \ @@ -187,7 +187,7 @@ _base_basectl_completion() { '-v[Enable DEBUG logging]' '(-h --help)'{-h,--help}'[Show help text]' ;; check) - _arguments '--profile[Include prerequisite profiles]:profile:(dev sre ai dev,sre dev,ai sre,ai dev,sre,ai)' \ + _arguments '--profile[Include prerequisite profiles]:profile:(dev sre ai linux-lab dev,sre dev,ai dev,linux-lab sre,ai sre,linux-lab ai,linux-lab dev,sre,ai dev,sre,linux-lab dev,ai,linux-lab sre,ai,linux-lab dev,sre,ai,linux-lab)' \ '--format[Output format]:format:(text json)' \ '--manifest[Use a specific manifest]:path:_files' \ '--remote-network[Opt in to bounded project Git origin reachability checks]' \ @@ -346,7 +346,7 @@ _base_basectl_completion() { _arguments '1:ci command:(setup check doctor)' \ '--format[Output format]:format:(text json)' \ '--manifest[Use a specific manifest]:path:_files' \ - '--profile[Include prerequisite profiles]:profile:(dev sre ai dev,sre dev,ai sre,ai dev,sre,ai)' \ + '--profile[Include prerequisite profiles]:profile:(dev sre ai linux-lab dev,sre dev,ai dev,linux-lab sre,ai sre,linux-lab ai,linux-lab dev,sre,ai dev,sre,linux-lab dev,ai,linux-lab sre,ai,linux-lab dev,sre,ai,linux-lab)' \ '--recreate-venv[Recreate the project virtual environment]' \ '-v[Enable DEBUG logging]' \ '(-h --help)'{-h,--help}'[Show help text]' \ @@ -356,7 +356,7 @@ _base_basectl_completion() { _arguments '1:ci command:(setup check doctor)' \ '--format[Output format]:format:(text json)' \ '--manifest[Use a specific manifest]:path:_files' \ - '--profile[Include prerequisite profiles]:profile:(dev sre ai dev,sre dev,ai sre,ai dev,sre,ai)' \ + '--profile[Include prerequisite profiles]:profile:(dev sre ai linux-lab dev,sre dev,ai dev,linux-lab sre,ai sre,linux-lab ai,linux-lab dev,sre,ai dev,sre,linux-lab dev,ai,linux-lab sre,ai,linux-lab dev,sre,ai,linux-lab)' \ '-v[Enable DEBUG logging]' \ '(-h --help)'{-h,--help}'[Show help text]' \ '2:Base project:->projects' @@ -404,7 +404,7 @@ _base_basectl_completion() { _arguments '1:config command:(path show doctor)' ;; doctor) - _arguments '--profile[Include prerequisite profiles]:profile:(dev sre ai dev,sre dev,ai sre,ai dev,sre,ai)' \ + _arguments '--profile[Include prerequisite profiles]:profile:(dev sre ai linux-lab dev,sre dev,ai dev,linux-lab sre,ai sre,linux-lab ai,linux-lab dev,sre,ai dev,sre,linux-lab dev,ai,linux-lab sre,ai,linux-lab dev,sre,ai,linux-lab)' \ '--format[Output format]:format:(text json)' \ '--manifest[Use a specific manifest]:path:_files' \ '--remote-network[Opt in to bounded project Git origin reachability diagnostics]' \ @@ -503,7 +503,7 @@ _base_basectl_completion() { esac ;; onboard) - _arguments '--profile[Include prerequisite profiles]:profile:(dev sre ai dev,sre dev,ai sre,ai dev,sre,ai)' \ + _arguments '--profile[Include prerequisite profiles]:profile:(dev sre ai linux-lab dev,sre dev,ai dev,linux-lab sre,ai sre,linux-lab ai,linux-lab dev,sre,ai dev,sre,linux-lab dev,ai,linux-lab sre,ai,linux-lab dev,sre,ai,linux-lab)' \ '--dry-run[Explain planned onboarding steps without making changes]' \ '--yes[Accept default answers for setup and shell profile prompts]' \ '--no-profile[Skip shell profile updates]' \