From b168761b752c0d0022ac107e0c344587b3a39706 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 6 Feb 2026 13:31:49 +0100 Subject: [PATCH 1/3] Add install script --- .github/workflows/dev-containers.yml | 14 +- README.md | 34 +- scripts/install.sh | 672 +++++++++++++++++++++++++++ scripts/install.test.sh | 580 +++++++++++++++++++++++ 4 files changed, 1297 insertions(+), 3 deletions(-) create mode 100755 scripts/install.sh create mode 100755 scripts/install.test.sh diff --git a/.github/workflows/dev-containers.yml b/.github/workflows/dev-containers.yml index b635d259c..d1f43995e 100644 --- a/.github/workflows/dev-containers.yml +++ b/.github/workflows/dev-containers.yml @@ -120,9 +120,21 @@ jobs: FEATURES_TEST__AZURE_REGISTRY_SCOPED_CREDENTIAL: ${{ secrets.FEATURES_TEST__AZURE_REGISTRY_SCOPED_CREDENTIAL }} + install-script: + name: Install Script + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v6 + - name: Run install.sh tests + run: sh scripts/install.test.sh + tests: name: Tests - needs: [tests-matrix, features-registry-compatibility] + needs: [tests-matrix, features-registry-compatibility, install-script] runs-on: ubuntu-latest steps: - name: Done diff --git a/README.md b/README.md index d59523f83..622decd57 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,42 @@ This CLI is in active development. Current status: ## Try it out -We'd love for you to try out the dev container CLI and let us know what you think. You can quickly try it out in just a few simple steps, either by installing its npm package or building the CLI repo from sources (see "[Build from sources](#build-from-sources)"). +We'd love for you to try out the dev container CLI and let us know what you think. You can quickly try it out in just a few simple steps, either by using the install script, installing its npm package, or building the CLI repo from sources (see "[Build from sources](#build-from-sources)"). -To install the npm package you will need Python and C/C++ installed to build one of the dependencies (see, e.g., [here](https://github.com/microsoft/vscode/wiki/How-to-Contribute) for instructions). +### Install script + +You can install the CLI with a standalone script that downloads a bundled Node.js runtime, so no pre-installed Node.js is required. It works on Linux and macOS (x64 and arm64): + +```bash +curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh +``` + +Then add the install location to your PATH: + +```bash +export PATH="$HOME/.devcontainers/bin:$PATH" +``` + +You can also specify a version, a custom install directory, or update/uninstall an existing installation: + +```bash +# Install a specific version +sh install.sh --version 0.82.0 + +# Install to a custom directory +sh install.sh --prefix ~/.local/devcontainers + +# Update to latest +sh install.sh --update + +# Uninstall +sh install.sh --uninstall +``` ### npm install +To install the npm package you will need Python and C/C++ installed to build one of the dependencies (see, e.g., [here](https://github.com/microsoft/vscode/wiki/How-to-Contribute) for instructions). + ```bash npm install -g @devcontainers/cli ``` diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 000000000..ab048dbc9 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,672 @@ +#!/bin/sh +# install.sh - Install @devcontainers/cli with bundled Node.js +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh +# wget -qO- https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh +# +# Options: +# --prefix DIR Installation directory (default: ~/.devcontainers) +# --version VER CLI version to install (default: latest) +# --node-version VER Node.js major version (default: 20) +# --update Update existing installation to latest versions +# --uninstall Remove the installation +# --help Show this help message +# +# Environment: +# DEVCONTAINERS_INSTALL_DIR Override default installation directory + +set -e + +# Default configuration +INSTALL_PREFIX="${DEVCONTAINERS_INSTALL_DIR:-$HOME/.devcontainers}" +CLI_VERSION="latest" +NODE_MAJOR_VERSION="20" +UPDATE_MODE=false +UNINSTALL_MODE=false + +# Terminal colors (disabled if not a tty) +setup_colors() { + if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + BLUE='\033[0;34m' + BOLD='\033[1m' + RESET='\033[0m' + else + RED='' + GREEN='' + YELLOW='' + BLUE='' + BOLD='' + RESET='' + fi +} + +say() { + printf '%b\n' "${GREEN}>${RESET} $1" +} + +warn() { + printf '%b\n' "${YELLOW}warning${RESET}: $1" >&2 +} + +error() { + printf '%b\n' "${RED}error${RESET}: $1" >&2 +} + +# Print usage information +usage() { + cat << 'EOF' +Install @devcontainers/cli with bundled Node.js + +Usage: + curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh + sh install.sh [OPTIONS] + +Options: + --prefix DIR Installation directory (default: ~/.devcontainers) + --version VER CLI version to install (default: latest) + --node-version VER Node.js major version (default: 20) + --update Update existing installation to latest versions + --uninstall Remove the installation + --help Show this help message + +Environment: + DEVCONTAINERS_INSTALL_DIR Override default installation directory + +Examples: + # Install latest version + curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh + + # Install specific version + sh install.sh --version 0.82.0 + + # Install to custom directory + sh install.sh --prefix ~/.local/devcontainers + + # Update existing installation + sh install.sh --update + + # Uninstall + sh install.sh --uninstall + +After installation, add to your shell profile: + export PATH="$HOME/.devcontainers/bin:$PATH" +EOF +} + +# Parse command-line arguments +parse_args() { + while [ $# -gt 0 ]; do + case "$1" in + --prefix) + INSTALL_PREFIX="$2" + shift 2 + ;; + --prefix=*) + INSTALL_PREFIX="${1#*=}" + shift + ;; + --version) + CLI_VERSION="$2" + shift 2 + ;; + --version=*) + CLI_VERSION="${1#*=}" + shift + ;; + --node-version) + NODE_MAJOR_VERSION="$2" + shift 2 + ;; + --node-version=*) + NODE_MAJOR_VERSION="${1#*=}" + shift + ;; + --update) + UPDATE_MODE=true + shift + ;; + --uninstall) + UNINSTALL_MODE=true + shift + ;; + --help|-h) + usage + exit 0 + ;; + *) + error "Unknown option: $1" + usage + exit 1 + ;; + esac + done +} + +# Detect platform (OS and architecture) +detect_platform() { + # OS detection + case "$(uname -s)" in + Linux*) + PLATFORM="linux" + ;; + Darwin*) + PLATFORM="darwin" + ;; + CYGWIN*|MINGW*|MSYS*) + error "Windows is not supported by this installer." + error "Please use WSL (Windows Subsystem for Linux) or install via npm:" + error " npm install -g @devcontainers/cli" + exit 1 + ;; + *) + error "Unsupported operating system: $(uname -s)" + exit 1 + ;; + esac + + # Architecture detection + case "$(uname -m)" in + x86_64|amd64) + ARCH="x64" + ;; + aarch64|arm64) + ARCH="arm64" + ;; + armv7l|armv6l) + error "32-bit ARM is not supported." + exit 1 + ;; + *) + error "Unsupported architecture: $(uname -m)" + exit 1 + ;; + esac + + # macOS: Detect if running under Rosetta 2 and prefer native arm64 + if [ "$PLATFORM" = "darwin" ] && [ "$ARCH" = "x64" ]; then + if sysctl -n sysctl.proc_translated 2>/dev/null | grep -q 1; then + say "Detected Rosetta 2 translation, using native arm64 binary" + ARCH="arm64" + fi + fi +} + +# Check for required tools +check_prerequisites() { + # Check for curl or wget + if command -v curl >/dev/null 2>&1; then + DOWNLOADER="curl" + elif command -v wget >/dev/null 2>&1; then + DOWNLOADER="wget" + else + error "Either 'curl' or 'wget' is required but neither was found." + exit 1 + fi + + # Check for tar + if ! command -v tar >/dev/null 2>&1; then + error "'tar' is required but not found." + exit 1 + fi + + # Check if we can write to the install directory + if [ -e "$INSTALL_PREFIX" ]; then + if [ ! -d "$INSTALL_PREFIX" ]; then + error "Installation path exists but is not a directory: $INSTALL_PREFIX" + exit 1 + fi + if [ ! -w "$INSTALL_PREFIX" ]; then + error "No write permission for installation directory: $INSTALL_PREFIX" + exit 1 + fi + else + # Check if we can create the directory + PARENT_DIR="$(dirname "$INSTALL_PREFIX")" + if [ ! -w "$PARENT_DIR" ]; then + error "No write permission to create installation directory: $INSTALL_PREFIX" + exit 1 + fi + fi +} + +# Download a file using curl or wget +download() { + url="$1" + output="$2" + + if [ "$DOWNLOADER" = "curl" ]; then + curl -fSL --retry 3 --retry-delay 2 -o "$output" "$url" + else + wget --tries=3 --waitretry=2 -q -O "$output" "$url" + fi +} + +# Fetch content from a URL (for API calls) +fetch() { + url="$1" + + if [ "$DOWNLOADER" = "curl" ]; then + curl -fsSL "$url" + else + wget -qO- "$url" + fi +} + +# Resolve "latest" CLI version from npm registry +resolve_cli_version() { + if [ "$CLI_VERSION" = "latest" ]; then + say "Resolving latest CLI version..." + version=$(fetch "https://registry.npmjs.org/@devcontainers/cli/latest" | \ + sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) + if [ -z "$version" ]; then + error "Failed to resolve latest CLI version from npm registry" + exit 1 + fi + CLI_VERSION="$version" + fi + say "CLI version: $CLI_VERSION" +} + +# Resolve full Node.js version from major version +resolve_node_version() { + say "Resolving Node.js v$NODE_MAJOR_VERSION LTS version..." + + # Get the latest version for the major version + index_url="https://nodejs.org/dist/index.json" + version=$(fetch "$index_url" | \ + sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"v\('"$NODE_MAJOR_VERSION"'\.[^"]*\)".*/\1/p' | head -1) + + if [ -z "$version" ]; then + error "Failed to resolve Node.js v$NODE_MAJOR_VERSION version" + exit 1 + fi + + NODE_VERSION="$version" + say "Node.js version: v$NODE_VERSION" +} + +# Get Node.js download URL +get_node_url() { + # Prefer .tar.xz if available, fall back to .tar.gz + echo "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-${PLATFORM}-${ARCH}.tar.xz" +} + +# Get CLI download URL from npm registry +get_cli_url() { + echo "https://registry.npmjs.org/@devcontainers/cli/-/cli-${CLI_VERSION}.tgz" +} + +# Install Node.js +install_node() { + node_dir="$INSTALL_PREFIX/node" + version_dir="$node_dir/v$NODE_VERSION" + + # Check if already installed + if [ -d "$version_dir" ] && [ -x "$version_dir/bin/node" ]; then + say "Node.js v$NODE_VERSION is already installed" + else + say "Downloading Node.js v$NODE_VERSION..." + + tmp_dir=$(mktemp -d) + trap 'rm -rf "$tmp_dir"' EXIT + + node_url=$(get_node_url) + tarball="$tmp_dir/node.tar.xz" + + if ! download "$node_url" "$tarball"; then + # Try .tar.gz if .tar.xz failed + node_url="${node_url%.xz}.gz" + tarball="$tmp_dir/node.tar.gz" + say "Trying .tar.gz format..." + download "$node_url" "$tarball" + fi + + say "Extracting Node.js..." + mkdir -p "$node_dir" + + # Extract to temp first, then move + extract_dir="$tmp_dir/extracted" + mkdir -p "$extract_dir" + + case "$tarball" in + *.xz) + # Try xz decompression + if command -v xz >/dev/null 2>&1; then + xz -d -c "$tarball" | tar -xf - -C "$extract_dir" + else + # Some tar implementations support -J for xz + tar -xJf "$tarball" -C "$extract_dir" 2>/dev/null || { + error "xz decompression not available. Please install xz-utils." + exit 1 + } + fi + ;; + *.gz) + tar -xzf "$tarball" -C "$extract_dir" + ;; + esac + + # Move extracted directory to version directory + mv "$extract_dir"/node-v*/* "$extract_dir"/ + rmdir "$extract_dir"/node-v* 2>/dev/null || true + mkdir -p "$version_dir" + mv "$extract_dir"/* "$version_dir"/ + + trap - EXIT + rm -rf "$tmp_dir" + fi + + # Update current symlink + say "Activating Node.js v$NODE_VERSION..." + ln -sfn "v$NODE_VERSION" "$node_dir/current" + + # Save metadata + mkdir -p "$INSTALL_PREFIX/.metadata" + echo "$NODE_VERSION" > "$INSTALL_PREFIX/.metadata/node-version" +} + +# Install CLI +install_cli() { + cli_dir="$INSTALL_PREFIX/cli" + version_dir="$cli_dir/$CLI_VERSION" + + # Check if already installed + if [ -d "$version_dir/package" ] && [ -f "$version_dir/package/devcontainer.js" ]; then + say "CLI v$CLI_VERSION is already installed" + else + say "Downloading CLI v$CLI_VERSION..." + + tmp_dir=$(mktemp -d) + trap 'rm -rf "$tmp_dir"' EXIT + + cli_url=$(get_cli_url) + tarball="$tmp_dir/cli.tgz" + + download "$cli_url" "$tarball" + + say "Extracting CLI..." + mkdir -p "$version_dir" + tar -xzf "$tarball" -C "$version_dir" + + trap - EXIT + rm -rf "$tmp_dir" + fi + + # Update current symlink + say "Activating CLI v$CLI_VERSION..." + ln -sfn "$CLI_VERSION" "$cli_dir/current" + + # Save metadata + mkdir -p "$INSTALL_PREFIX/.metadata" + echo "$CLI_VERSION" > "$INSTALL_PREFIX/.metadata/installed-version" +} + +# Create wrapper script +create_wrapper() { + bin_dir="$INSTALL_PREFIX/bin" + wrapper="$bin_dir/devcontainer" + + say "Creating wrapper script..." + mkdir -p "$bin_dir" + + cat > "$wrapper" << 'WRAPPER_EOF' +#!/bin/sh +# devcontainer CLI wrapper - generated by install.sh +# https://github.com/devcontainers/cli + +set -e + +# Resolve the installation directory +# Handle both direct execution and symlinked scenarios +if [ -L "$0" ]; then + # Follow symlink + SCRIPT_PATH="$(readlink "$0" 2>/dev/null || readlink -f "$0" 2>/dev/null || echo "$0")" +else + SCRIPT_PATH="$0" +fi + +# Get absolute path to script directory +SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)" +INSTALL_DIR="$(dirname "$SCRIPT_DIR")" + +# Paths to bundled components +NODE_BIN="$INSTALL_DIR/node/current/bin/node" +CLI_ENTRY="$INSTALL_DIR/cli/current/package/devcontainer.js" + +# Verify Node.js exists +if [ ! -x "$NODE_BIN" ]; then + echo "Error: Node.js not found at $NODE_BIN" >&2 + echo "Installation may be corrupted. Please reinstall:" >&2 + echo " curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh" >&2 + exit 1 +fi + +# Verify CLI exists +if [ ! -f "$CLI_ENTRY" ]; then + echo "Error: CLI not found at $CLI_ENTRY" >&2 + echo "Installation may be corrupted. Please reinstall:" >&2 + echo " curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh" >&2 + exit 1 +fi + +# Execute the CLI with bundled Node.js +exec "$NODE_BIN" "$CLI_ENTRY" "$@" +WRAPPER_EOF + + chmod +x "$wrapper" +} + +# Verify installation +verify_installation() { + say "Verifying installation..." + + node_bin="$INSTALL_PREFIX/node/current/bin/node" + cli_entry="$INSTALL_PREFIX/cli/current/package/devcontainer.js" + wrapper="$INSTALL_PREFIX/bin/devcontainer" + + if [ ! -x "$node_bin" ]; then + error "Node.js binary not found or not executable" + exit 1 + fi + + if [ ! -f "$cli_entry" ]; then + error "CLI entry point not found" + exit 1 + fi + + if [ ! -x "$wrapper" ]; then + error "Wrapper script not found or not executable" + exit 1 + fi + + # Try to get version + version=$("$wrapper" --version 2>/dev/null || true) + if [ -n "$version" ]; then + say "Installed: devcontainer $version" + else + warn "Could not verify CLI version, but files are in place" + fi +} + +# Check for existing installation and warn about conflicts +check_existing() { + # Check for existing devcontainer in PATH + existing=$(command -v devcontainer 2>/dev/null || true) + if [ -n "$existing" ]; then + # Check if it's our installation + case "$existing" in + "$INSTALL_PREFIX"*) + # It's our installation, that's fine + ;; + *) + warn "Found existing devcontainer at: $existing" + warn "After installation, ensure $INSTALL_PREFIX/bin is first in your PATH" + ;; + esac + fi + + # Check for existing installation directory + if [ -d "$INSTALL_PREFIX" ] && [ ! "$UPDATE_MODE" = true ]; then + if [ -f "$INSTALL_PREFIX/.metadata/installed-version" ]; then + current_version=$(cat "$INSTALL_PREFIX/.metadata/installed-version") + say "Found existing installation: v$current_version" + say "Use --update to update, or --uninstall to remove first" + fi + fi +} + +# Update existing installation +do_update() { + if [ ! -d "$INSTALL_PREFIX" ] || [ ! -f "$INSTALL_PREFIX/.metadata/installed-version" ]; then + error "No existing installation found at $INSTALL_PREFIX" + error "Run without --update to perform a fresh installation" + exit 1 + fi + + current_cli=$(cat "$INSTALL_PREFIX/.metadata/installed-version" 2>/dev/null || echo "unknown") + current_node=$(cat "$INSTALL_PREFIX/.metadata/node-version" 2>/dev/null || echo "unknown") + + say "Current installation:" + say " CLI: v$current_cli" + say " Node.js: v$current_node" + + # Resolve latest versions + CLI_VERSION="latest" + resolve_cli_version + resolve_node_version + + # Update components + if [ "$current_cli" = "$CLI_VERSION" ]; then + say "CLI is already up to date" + else + say "Updating CLI: v$current_cli -> v$CLI_VERSION" + install_cli + fi + + if [ "$current_node" = "$NODE_VERSION" ]; then + say "Node.js is already up to date" + else + say "Updating Node.js: v$current_node -> v$NODE_VERSION" + install_node + fi + + # Recreate wrapper in case it changed + create_wrapper + verify_installation +} + +# Uninstall +do_uninstall() { + if [ ! -d "$INSTALL_PREFIX" ]; then + say "Nothing to uninstall at $INSTALL_PREFIX" + exit 0 + fi + + say "Uninstalling from $INSTALL_PREFIX..." + rm -rf "$INSTALL_PREFIX" + say "Uninstallation complete" + say "" + say "Don't forget to remove the PATH entry from your shell profile:" + say " export PATH=\"$INSTALL_PREFIX/bin:\$PATH\"" +} + +# Print post-installation instructions +print_instructions() { + bin_path="$INSTALL_PREFIX/bin" + + echo "" + say "${BOLD}Installation complete!${RESET}" + echo "" + + # Check if already in PATH + case ":$PATH:" in + *":$bin_path:"*) + say "The installation directory is already in your PATH." + say "You can now use: devcontainer --help" + ;; + *) + say "Add the following to your shell profile to use devcontainer:" + echo "" + echo " export PATH=\"$bin_path:\$PATH\"" + echo "" + + # Detect shell and suggest profile file + shell_name=$(basename "${SHELL:-/bin/sh}") + case "$shell_name" in + bash) + if [ -f "$HOME/.bash_profile" ]; then + say "For bash, add to: ~/.bash_profile" + else + say "For bash, add to: ~/.bashrc" + fi + ;; + zsh) + say "For zsh, add to: ~/.zshrc" + ;; + fish) + say "For fish, run:" + echo " fish_add_path $bin_path" + ;; + *) + say "Add to your shell's profile file" + ;; + esac + echo "" + say "Then restart your shell or run:" + echo " export PATH=\"$bin_path:\$PATH\"" + ;; + esac + + echo "" + say "To update:" + echo " curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh -s -- --update" + say "To uninstall:" + echo " curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh -s -- --uninstall" + say "Or simply: rm -rf $INSTALL_PREFIX" +} + +# Main function +main() { + setup_colors + parse_args "$@" + + echo "" + say "${BOLD}@devcontainers/cli installer${RESET}" + echo "" + + # Handle uninstall + if [ "$UNINSTALL_MODE" = true ]; then + do_uninstall + exit 0 + fi + + detect_platform + say "Platform: $PLATFORM-$ARCH" + say "Install directory: $INSTALL_PREFIX" + + check_prerequisites + check_existing + + # Handle update + if [ "$UPDATE_MODE" = true ]; then + do_update + print_instructions + exit 0 + fi + + # Fresh installation + resolve_cli_version + resolve_node_version + + install_node + install_cli + create_wrapper + verify_installation + print_instructions +} + +main "$@" diff --git a/scripts/install.test.sh b/scripts/install.test.sh new file mode 100755 index 000000000..68c9fb51f --- /dev/null +++ b/scripts/install.test.sh @@ -0,0 +1,580 @@ +#!/bin/sh +# install.test.sh - Tests for install.sh +# +# Usage: +# sh scripts/install.test.sh +# +# Can be run in CI or locally. Uses a temp directory for all installs. +# Requires network access to download Node.js and the CLI package. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +INSTALL_SCRIPT="$SCRIPT_DIR/install.sh" + +# ── Test framework ──────────────────────────────────────────────── + +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 +FAILED_NAMES="" + +# Colors (disabled in non-tty / CI) +if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then + C_RED='\033[0;31m' + C_GREEN='\033[0;32m' + C_YELLOW='\033[0;33m' + C_BOLD='\033[1m' + C_RESET='\033[0m' +else + C_RED='' + C_GREEN='' + C_YELLOW='' + C_BOLD='' + C_RESET='' +fi + +pass() { + TESTS_PASSED=$((TESTS_PASSED + 1)) + printf '%b\n' " ${C_GREEN}✓${C_RESET} $1" +} + +fail() { + TESTS_FAILED=$((TESTS_FAILED + 1)) + FAILED_NAMES="$FAILED_NAMES\n - $1" + printf '%b\n' " ${C_RED}✗${C_RESET} $1" + if [ -n "${2:-}" ]; then + printf ' %s\n' "$2" + fi +} + +assert_eq() { + expected="$1" + actual="$2" + msg="$3" + TESTS_RUN=$((TESTS_RUN + 1)) + if [ "$expected" = "$actual" ]; then + pass "$msg" + else + fail "$msg" "expected: '$expected', got: '$actual'" + fi +} + +assert_contains() { + haystack="$1" + needle="$2" + msg="$3" + TESTS_RUN=$((TESTS_RUN + 1)) + case "$haystack" in + *"$needle"*) + pass "$msg" + ;; + *) + fail "$msg" "expected output to contain: '$needle'" + ;; + esac +} + +assert_file_exists() { + path="$1" + msg="$2" + TESTS_RUN=$((TESTS_RUN + 1)) + if [ -f "$path" ]; then + pass "$msg" + else + fail "$msg" "file not found: $path" + fi +} + +assert_dir_exists() { + path="$1" + msg="$2" + TESTS_RUN=$((TESTS_RUN + 1)) + if [ -d "$path" ]; then + pass "$msg" + else + fail "$msg" "directory not found: $path" + fi +} + +assert_executable() { + path="$1" + msg="$2" + TESTS_RUN=$((TESTS_RUN + 1)) + if [ -x "$path" ]; then + pass "$msg" + else + fail "$msg" "not executable: $path" + fi +} + +assert_symlink() { + path="$1" + msg="$2" + TESTS_RUN=$((TESTS_RUN + 1)) + if [ -L "$path" ]; then + pass "$msg" + else + fail "$msg" "not a symlink: $path" + fi +} + +assert_exit_code() { + expected="$1" + actual="$2" + msg="$3" + TESTS_RUN=$((TESTS_RUN + 1)) + if [ "$expected" = "$actual" ]; then + pass "$msg" + else + fail "$msg" "expected exit code $expected, got $actual" + fi +} + +# ── Setup / teardown ───────────────────────────────────────────── + +TEST_TMPDIR="" +setup() { + TEST_TMPDIR="$(mktemp -d)" +} + +teardown() { + if [ -n "$TEST_TMPDIR" ] && [ -d "$TEST_TMPDIR" ]; then + rm -rf "$TEST_TMPDIR" + fi +} + +# ── Tests: --help ───────────────────────────────────────────────── + +test_help_flag() { + printf '%b\n' "${C_BOLD}--help flag${C_RESET}" + setup + + output=$(sh "$INSTALL_SCRIPT" --help 2>&1) || true + + assert_contains "$output" "Install @devcontainers/cli" "--help shows description" + assert_contains "$output" "--prefix" "--help shows --prefix option" + assert_contains "$output" "--version" "--help shows --version option" + assert_contains "$output" "--node-version" "--help shows --node-version option" + assert_contains "$output" "--update" "--help shows --update option" + assert_contains "$output" "--uninstall" "--help shows --uninstall option" + assert_contains "$output" "DEVCONTAINERS_INSTALL_DIR" "--help shows env var" + + teardown +} + +test_help_short_flag() { + printf '%b\n' "${C_BOLD}-h flag${C_RESET}" + setup + + output=$(sh "$INSTALL_SCRIPT" -h 2>&1) || true + assert_contains "$output" "Install @devcontainers/cli" "-h shows help" + + teardown +} + +# ── Tests: argument parsing errors ──────────────────────────────── + +test_unknown_option() { + printf '%b\n' "${C_BOLD}Unknown option${C_RESET}" + setup + + output=$(sh "$INSTALL_SCRIPT" --bogus 2>&1) && rc=0 || rc=$? + assert_exit_code "1" "$rc" "exits with code 1 on unknown option" + assert_contains "$output" "Unknown option" "reports unknown option" + + teardown +} + +# ── Tests: --uninstall on missing dir ───────────────────────────── + +test_uninstall_no_dir() { + printf '%b\n' "${C_BOLD}Uninstall with no existing installation${C_RESET}" + setup + + prefix="$TEST_TMPDIR/nonexistent" + output=$(sh "$INSTALL_SCRIPT" --prefix "$prefix" --uninstall 2>&1) && rc=0 || rc=$? + assert_exit_code "0" "$rc" "exits 0 when nothing to uninstall" + assert_contains "$output" "Nothing to uninstall" "reports nothing to uninstall" + + teardown +} + +# ── Tests: --update on missing installation ─────────────────────── + +test_update_no_installation() { + printf '%b\n' "${C_BOLD}Update with no existing installation${C_RESET}" + setup + + prefix="$TEST_TMPDIR/empty" + mkdir -p "$prefix" + output=$(sh "$INSTALL_SCRIPT" --prefix "$prefix" --update 2>&1) && rc=0 || rc=$? + assert_exit_code "1" "$rc" "exits 1 when no installation found" + assert_contains "$output" "No existing installation" "reports missing installation" + + teardown +} + +# ── Tests: DEVCONTAINERS_INSTALL_DIR env var ────────────────────── + +test_env_var_prefix() { + printf '%b\n' "${C_BOLD}DEVCONTAINERS_INSTALL_DIR env var${C_RESET}" + setup + + # The env var should be reflected in the help output or the + # install run. We just test that the script picks it up by + # running --uninstall (lightweight) against a nonexistent path. + prefix="$TEST_TMPDIR/from-env" + output=$(DEVCONTAINERS_INSTALL_DIR="$prefix" sh "$INSTALL_SCRIPT" --uninstall 2>&1) && rc=0 || rc=$? + assert_exit_code "0" "$rc" "exits 0 with env-var prefix" + assert_contains "$output" "Nothing to uninstall" "uses env-var prefix path" + + teardown +} + +# ── Tests: --prefix flag overrides env var ──────────────────────── + +test_prefix_overrides_env() { + printf '%b\n' "${C_BOLD}--prefix overrides DEVCONTAINERS_INSTALL_DIR${C_RESET}" + setup + + env_dir="$TEST_TMPDIR/env-dir" + flag_dir="$TEST_TMPDIR/flag-dir" + output=$(DEVCONTAINERS_INSTALL_DIR="$env_dir" sh "$INSTALL_SCRIPT" --prefix "$flag_dir" --uninstall 2>&1) && rc=0 || rc=$? + assert_exit_code "0" "$rc" "exits 0" + # The output should reference flag_dir, not env_dir + assert_contains "$output" "Nothing to uninstall" "--prefix is used over env var" + + teardown +} + +# ── Tests: full install with a specific version ─────────────────── + +test_full_install() { + printf '%b\n' "${C_BOLD}Full install (specific CLI version)${C_RESET}" + setup + + prefix="$TEST_TMPDIR/devcontainers" + + # Use a known CLI version to make the test deterministic + cli_version="0.75.0" + + output=$(sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "$cli_version" 2>&1) && rc=0 || rc=$? + assert_exit_code "0" "$rc" "install exits 0" + + # Directory structure + assert_dir_exists "$prefix/bin" "bin/ directory created" + assert_dir_exists "$prefix/node" "node/ directory created" + assert_dir_exists "$prefix/cli" "cli/ directory created" + assert_dir_exists "$prefix/.metadata" ".metadata/ directory created" + + # Wrapper script + assert_file_exists "$prefix/bin/devcontainer" "wrapper script exists" + assert_executable "$prefix/bin/devcontainer" "wrapper script is executable" + + # Symlinks + assert_symlink "$prefix/node/current" "node/current is a symlink" + assert_symlink "$prefix/cli/current" "cli/current is a symlink" + + # Node.js binary + assert_executable "$prefix/node/current/bin/node" "node binary is executable" + + # CLI entry point + assert_file_exists "$prefix/cli/current/package/devcontainer.js" "CLI entry point exists" + + # Metadata + assert_file_exists "$prefix/.metadata/installed-version" "CLI version metadata written" + assert_file_exists "$prefix/.metadata/node-version" "Node version metadata written" + + installed_version=$(cat "$prefix/.metadata/installed-version") + assert_eq "$cli_version" "$installed_version" "metadata records correct CLI version" + + # Wrapper executes successfully + version_output=$("$prefix/bin/devcontainer" --version 2>/dev/null) && wrc=0 || wrc=$? + assert_exit_code "0" "$wrc" "wrapper --version exits 0" + assert_contains "$version_output" "$cli_version" "wrapper reports installed version" + + teardown +} + +# ── Tests: idempotent install ───────────────────────────────────── + +test_idempotent_install() { + printf '%b\n' "${C_BOLD}Idempotent install (run twice)${C_RESET}" + setup + + prefix="$TEST_TMPDIR/devcontainers" + cli_version="0.75.0" + + # First install + sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "$cli_version" >/dev/null 2>&1 + + # Second install – same version, should succeed and say "already installed" + output=$(sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "$cli_version" 2>&1) && rc=0 || rc=$? + assert_exit_code "0" "$rc" "second install exits 0" + assert_contains "$output" "already installed" "detects existing Node.js or CLI" + + # Still works + version_output=$("$prefix/bin/devcontainer" --version 2>/dev/null) && wrc=0 || wrc=$? + assert_exit_code "0" "$wrc" "wrapper still works after second install" + + teardown +} + +# ── Tests: uninstall after install ──────────────────────────────── + +test_uninstall_after_install() { + printf '%b\n' "${C_BOLD}Uninstall removes installation${C_RESET}" + setup + + prefix="$TEST_TMPDIR/devcontainers" + cli_version="0.75.0" + + # Install + sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "$cli_version" >/dev/null 2>&1 + + # Uninstall + output=$(sh "$INSTALL_SCRIPT" --prefix "$prefix" --uninstall 2>&1) && rc=0 || rc=$? + assert_exit_code "0" "$rc" "uninstall exits 0" + assert_contains "$output" "Uninstallation complete" "reports completion" + + # Directory should be gone + TESTS_RUN=$((TESTS_RUN + 1)) + if [ ! -d "$prefix" ]; then + pass "install directory removed" + else + fail "install directory removed" "directory still exists: $prefix" + fi + + teardown +} + +# ── Tests: update existing installation ─────────────────────────── + +test_update_existing() { + printf '%b\n' "${C_BOLD}Update existing installation${C_RESET}" + setup + + prefix="$TEST_TMPDIR/devcontainers" + + # Install an older version first + old_version="0.72.0" + sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "$old_version" >/dev/null 2>&1 + + installed=$(cat "$prefix/.metadata/installed-version") + assert_eq "$old_version" "$installed" "initial version installed" + + # Update to a slightly newer specific version + new_version="0.75.0" + # Fake update by doing a fresh install with --version (--update resolves "latest") + sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "$new_version" >/dev/null 2>&1 + + updated=$(cat "$prefix/.metadata/installed-version") + assert_eq "$new_version" "$updated" "version updated in metadata" + + # Wrapper reports new version + version_output=$("$prefix/bin/devcontainer" --version 2>/dev/null) && wrc=0 || wrc=$? + assert_exit_code "0" "$wrc" "wrapper works after version change" + assert_contains "$version_output" "$new_version" "wrapper reports new version" + + teardown +} + +# ── Tests: wrapper handles missing node gracefully ──────────────── + +test_wrapper_missing_node() { + printf '%b\n' "${C_BOLD}Wrapper error when Node.js missing${C_RESET}" + setup + + prefix="$TEST_TMPDIR/devcontainers" + cli_version="0.75.0" + + # Install + sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "$cli_version" >/dev/null 2>&1 + + # Remove node binary + rm -rf "$prefix/node" + + output=$("$prefix/bin/devcontainer" --version 2>&1) && rc=0 || rc=$? + assert_exit_code "1" "$rc" "wrapper exits 1 when node missing" + assert_contains "$output" "Node.js not found" "wrapper reports missing Node.js" + + teardown +} + +# ── Tests: wrapper handles missing CLI gracefully ───────────────── + +test_wrapper_missing_cli() { + printf '%b\n' "${C_BOLD}Wrapper error when CLI missing${C_RESET}" + setup + + prefix="$TEST_TMPDIR/devcontainers" + cli_version="0.75.0" + + # Install + sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "$cli_version" >/dev/null 2>&1 + + # Remove CLI + rm -rf "$prefix/cli" + + output=$("$prefix/bin/devcontainer" --version 2>&1) && rc=0 || rc=$? + assert_exit_code "1" "$rc" "wrapper exits 1 when CLI missing" + assert_contains "$output" "CLI not found" "wrapper reports missing CLI" + + teardown +} + +# ── Tests: install via symlinked wrapper ────────────────────────── + +test_wrapper_via_symlink() { + printf '%b\n' "${C_BOLD}Wrapper works when invoked via symlink${C_RESET}" + setup + + prefix="$TEST_TMPDIR/devcontainers" + cli_version="0.75.0" + + sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "$cli_version" >/dev/null 2>&1 + + # Create a symlink to the wrapper in a different directory + link_dir="$TEST_TMPDIR/links" + mkdir -p "$link_dir" + ln -s "$prefix/bin/devcontainer" "$link_dir/devcontainer" + + version_output=$("$link_dir/devcontainer" --version 2>/dev/null) && wrc=0 || wrc=$? + assert_exit_code "0" "$wrc" "symlinked wrapper exits 0" + assert_contains "$version_output" "$cli_version" "symlinked wrapper reports version" + + teardown +} + +# ── Tests: install to path with spaces ──────────────────────────── + +test_path_with_spaces() { + printf '%b\n' "${C_BOLD}Install to path with spaces${C_RESET}" + setup + + prefix="$TEST_TMPDIR/my dev containers" + + cli_version="0.75.0" + output=$(sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "$cli_version" 2>&1) && rc=0 || rc=$? + assert_exit_code "0" "$rc" "install to spaced path exits 0" + + assert_file_exists "$prefix/bin/devcontainer" "wrapper exists in spaced path" + + version_output=$("$prefix/bin/devcontainer" --version 2>/dev/null) && wrc=0 || wrc=$? + assert_exit_code "0" "$wrc" "wrapper works from spaced path" + assert_contains "$version_output" "$cli_version" "reports correct version from spaced path" + + teardown +} + +# ── Tests: non-writable prefix ──────────────────────────────────── + +test_non_writable_prefix() { + printf '%b\n' "${C_BOLD}Error on non-writable prefix${C_RESET}" + setup + + # Skip if running as root (root can write anywhere) + if [ "$(id -u)" = "0" ]; then + TESTS_RUN=$((TESTS_RUN + 1)) + pass "skipped (running as root)" + teardown + return + fi + + prefix="/usr/local/no-permission-test-devcontainers-$$" + output=$(sh "$INSTALL_SCRIPT" --prefix "$prefix" --version "0.75.0" 2>&1) && rc=0 || rc=$? + assert_exit_code "1" "$rc" "exits 1 for non-writable prefix" + assert_contains "$output" "No write permission" "reports permission error" + + teardown +} + +# ── Tests: --prefix= form (equals delimiter) ───────────────────── + +test_prefix_equals_form() { + printf '%b\n' "${C_BOLD}--prefix=DIR form${C_RESET}" + setup + + prefix="$TEST_TMPDIR/eq-form" + # Just verify parsing works – use uninstall for a lightweight check + output=$(sh "$INSTALL_SCRIPT" "--prefix=$prefix" --uninstall 2>&1) && rc=0 || rc=$? + assert_exit_code "0" "$rc" "--prefix=DIR is accepted" + assert_contains "$output" "Nothing to uninstall" "--prefix=DIR path is used" + + teardown +} + +test_version_equals_form() { + printf '%b\n' "${C_BOLD}--version=VER form${C_RESET}" + setup + + prefix="$TEST_TMPDIR/ver-eq" + output=$(sh "$INSTALL_SCRIPT" "--prefix=$prefix" "--version=0.75.0" 2>&1) && rc=0 || rc=$? + assert_exit_code "0" "$rc" "--version=VER install exits 0" + assert_contains "$output" "0.75.0" "version from --version=VER is used" + + teardown +} + +# ── Run all tests ───────────────────────────────────────────────── + +printf '%b\n' "" +printf '%b\n' "${C_BOLD}install.sh test suite${C_RESET}" +printf '%b\n' "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +printf '%b\n' "" + +# Fast tests (no network required) +test_help_flag +printf '\n' +test_help_short_flag +printf '\n' +test_unknown_option +printf '\n' +test_uninstall_no_dir +printf '\n' +test_update_no_installation +printf '\n' +test_env_var_prefix +printf '\n' +test_prefix_overrides_env +printf '\n' +test_prefix_equals_form +printf '\n' +test_non_writable_prefix +printf '\n' + +# Integration tests (require network, download Node.js + CLI) +printf '%b\n' "${C_YELLOW}Integration tests (requires network)${C_RESET}" +printf '\n' +test_full_install +printf '\n' +test_idempotent_install +printf '\n' +test_uninstall_after_install +printf '\n' +test_update_existing +printf '\n' +test_wrapper_missing_node +printf '\n' +test_wrapper_missing_cli +printf '\n' +test_wrapper_via_symlink +printf '\n' +test_path_with_spaces +printf '\n' +test_version_equals_form +printf '\n' + +# ── Summary ─────────────────────────────────────────────────────── + +printf '%b\n' "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +if [ "$TESTS_FAILED" -eq 0 ]; then + printf '%b\n' "${C_GREEN}${C_BOLD}All $TESTS_RUN tests passed${C_RESET}" +else + printf '%b\n' "${C_RED}${C_BOLD}$TESTS_FAILED of $TESTS_RUN tests failed${C_RESET}" + printf '%b\n' "$FAILED_NAMES" +fi +printf '%b\n' "" + +exit "$TESTS_FAILED" From b0111ed283a9e93c5bdc79335c2aca3c08466944 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 6 Feb 2026 15:00:31 +0100 Subject: [PATCH 2/3] 0.83.0 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4f88234..598cb3e03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ Notable changes. +## February 2026 + +### [0.83.0] +- Add install script. (https://github.com/devcontainers/cli/pull/1142) +- Remove request body limit. (https://github.com/devcontainers/cli/pull/1141) +- Add BUILDKIT_INLINE_CACHE for container Feature path. (https://github.com/devcontainers/cli/pull/1135) + ## January 2026 ### [0.82.0] diff --git a/package.json b/package.json index 97e73fa11..81a4d54ba 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.82.0", + "version": "0.83.0", "bin": { "devcontainer": "devcontainer.js" }, From 4d42180ac71c81caf788d1c1eb511f7eb3e10ee4 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 6 Feb 2026 19:16:39 +0100 Subject: [PATCH 3/3] Consistent naming --- scripts/install.sh | 36 ++++++++++++++++++------------------ scripts/install.test.sh | 6 +++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index ab048dbc9..e44a36712 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -7,7 +7,7 @@ # # Options: # --prefix DIR Installation directory (default: ~/.devcontainers) -# --version VER CLI version to install (default: latest) +# --version VER Dev Containers CLI version to install (default: latest) # --node-version VER Node.js major version (default: 20) # --update Update existing installation to latest versions # --uninstall Remove the installation @@ -59,7 +59,7 @@ error() { # Print usage information usage() { cat << 'EOF' -Install @devcontainers/cli with bundled Node.js +Install the Dev Containers CLI with bundled Node.js Usage: curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh @@ -67,7 +67,7 @@ Usage: Options: --prefix DIR Installation directory (default: ~/.devcontainers) - --version VER CLI version to install (default: latest) + --version VER Dev Containers CLI version to install (default: latest) --node-version VER Node.js major version (default: 20) --update Update existing installation to latest versions --uninstall Remove the installation @@ -259,16 +259,16 @@ fetch() { # Resolve "latest" CLI version from npm registry resolve_cli_version() { if [ "$CLI_VERSION" = "latest" ]; then - say "Resolving latest CLI version..." + say "Resolving latest Dev Containers CLI version..." version=$(fetch "https://registry.npmjs.org/@devcontainers/cli/latest" | \ sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1) if [ -z "$version" ]; then - error "Failed to resolve latest CLI version from npm registry" + error "Failed to resolve latest Dev Containers CLI version from npm registry" exit 1 fi CLI_VERSION="$version" fi - say "CLI version: $CLI_VERSION" + say "Dev Containers CLI version: $CLI_VERSION" } # Resolve full Node.js version from major version @@ -376,9 +376,9 @@ install_cli() { # Check if already installed if [ -d "$version_dir/package" ] && [ -f "$version_dir/package/devcontainer.js" ]; then - say "CLI v$CLI_VERSION is already installed" + say "Dev Containers CLI v$CLI_VERSION is already installed" else - say "Downloading CLI v$CLI_VERSION..." + say "Downloading Dev Containers CLI v$CLI_VERSION..." tmp_dir=$(mktemp -d) trap 'rm -rf "$tmp_dir"' EXIT @@ -388,7 +388,7 @@ install_cli() { download "$cli_url" "$tarball" - say "Extracting CLI..." + say "Extracting Dev Containers CLI..." mkdir -p "$version_dir" tar -xzf "$tarball" -C "$version_dir" @@ -397,7 +397,7 @@ install_cli() { fi # Update current symlink - say "Activating CLI v$CLI_VERSION..." + say "Activating Dev Containers CLI v$CLI_VERSION..." ln -sfn "$CLI_VERSION" "$cli_dir/current" # Save metadata @@ -415,7 +415,7 @@ create_wrapper() { cat > "$wrapper" << 'WRAPPER_EOF' #!/bin/sh -# devcontainer CLI wrapper - generated by install.sh +# Dev Containers CLI wrapper - generated by install.sh # https://github.com/devcontainers/cli set -e @@ -447,7 +447,7 @@ fi # Verify CLI exists if [ ! -f "$CLI_ENTRY" ]; then - echo "Error: CLI not found at $CLI_ENTRY" >&2 + echo "Error: Dev Containers CLI not found at $CLI_ENTRY" >&2 echo "Installation may be corrupted. Please reinstall:" >&2 echo " curl -fsSL https://raw.githubusercontent.com/devcontainers/cli/main/scripts/install.sh | sh" >&2 exit 1 @@ -474,7 +474,7 @@ verify_installation() { fi if [ ! -f "$cli_entry" ]; then - error "CLI entry point not found" + error "Dev Containers CLI entry point not found" exit 1 fi @@ -488,7 +488,7 @@ verify_installation() { if [ -n "$version" ]; then say "Installed: devcontainer $version" else - warn "Could not verify CLI version, but files are in place" + warn "Could not verify Dev Containers CLI version, but files are in place" fi } @@ -531,7 +531,7 @@ do_update() { current_node=$(cat "$INSTALL_PREFIX/.metadata/node-version" 2>/dev/null || echo "unknown") say "Current installation:" - say " CLI: v$current_cli" + say " Dev Containers CLI: v$current_cli" say " Node.js: v$current_node" # Resolve latest versions @@ -541,9 +541,9 @@ do_update() { # Update components if [ "$current_cli" = "$CLI_VERSION" ]; then - say "CLI is already up to date" + say "Dev Containers CLI is already up to date" else - say "Updating CLI: v$current_cli -> v$CLI_VERSION" + say "Updating Dev Containers CLI: v$current_cli -> v$CLI_VERSION" install_cli fi @@ -635,7 +635,7 @@ main() { parse_args "$@" echo "" - say "${BOLD}@devcontainers/cli installer${RESET}" + say "${BOLD}Dev Containers CLI installer${RESET}" echo "" # Handle uninstall diff --git a/scripts/install.test.sh b/scripts/install.test.sh index 68c9fb51f..2a73b883e 100755 --- a/scripts/install.test.sh +++ b/scripts/install.test.sh @@ -152,7 +152,7 @@ test_help_flag() { output=$(sh "$INSTALL_SCRIPT" --help 2>&1) || true - assert_contains "$output" "Install @devcontainers/cli" "--help shows description" + assert_contains "$output" "Install the Dev Containers CLI" "--help shows description" assert_contains "$output" "--prefix" "--help shows --prefix option" assert_contains "$output" "--version" "--help shows --version option" assert_contains "$output" "--node-version" "--help shows --node-version option" @@ -168,7 +168,7 @@ test_help_short_flag() { setup output=$(sh "$INSTALL_SCRIPT" -h 2>&1) || true - assert_contains "$output" "Install @devcontainers/cli" "-h shows help" + assert_contains "$output" "Install the Dev Containers CLI" "-h shows help" teardown } @@ -419,7 +419,7 @@ test_wrapper_missing_cli() { output=$("$prefix/bin/devcontainer" --version 2>&1) && rc=0 || rc=$? assert_exit_code "1" "$rc" "wrapper exits 1 when CLI missing" - assert_contains "$output" "CLI not found" "wrapper reports missing CLI" + assert_contains "$output" "Dev Containers CLI not found" "wrapper reports missing Dev Containers CLI" teardown }