diff --git a/README.md b/README.md index 9e3e77b..9ba359a 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,20 @@ To run the tests: ```bash python -m pytest ``` +### Git hooks (pre-commit) + +This repository includes a `.pre-commit-config.yaml` to enforce code style and basic quality checks locally. + +```bash +# Install the pre-commit framework in your development environment +pip install pre-commit + +# Register the Git hooks for this repository +pre-commit install + +# (Optional) Run against all files once +pre-commit run --all-files +``` ### Troubleshooting - If editable install fails, ensure you have a modern toolchain: diff --git a/arm_cli/container/container.py b/arm_cli/container/container.py index b8ecd75..b4d48b2 100644 --- a/arm_cli/container/container.py +++ b/arm_cli/container/container.py @@ -1,4 +1,5 @@ import subprocess +import sys import click import docker @@ -17,8 +18,24 @@ def container(): def get_running_containers(): """Retrieve a list of running Docker containers""" - client = docker.from_env() - return client.containers.list(filters={"status": "running"}) + try: + client = docker.from_env() + return client.containers.list(filters={"status": "running"}) + except docker.errors.DockerException as e: + error_msg = str(e) + print("Error: Unable to connect to Docker daemon.", file=sys.stderr) + print(f"Details: {error_msg}", file=sys.stderr) + print("\nPossible solutions:", file=sys.stderr) + print(" 1. Ensure Docker daemon is running: sudo systemctl start docker", file=sys.stderr) + print( + " 2. Add your user to the docker group: sudo usermod -aG docker $USER", file=sys.stderr + ) + print(" (You'll need to log out and back in for this to take effect)", file=sys.stderr) + print(" 3. Check Docker socket permissions: ls -la /var/run/docker.sock", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: Unexpected error connecting to Docker: {e}", file=sys.stderr) + sys.exit(1) @container.command("list") @@ -116,6 +133,9 @@ def restart_container(ctx): container = client.containers.get(selected_container_name) container.restart() print(f"Container {selected_container_name} restarted successfully.") + except docker.errors.DockerException as e: + print(f"Error: Unable to connect to Docker daemon: {e}", file=sys.stderr) + sys.exit(1) except docker.errors.NotFound: print(f"Error: Container {selected_container_name} not found.") except docker.errors.APIError as e: @@ -156,6 +176,9 @@ def stop_container(ctx): container = client.containers.get(selected_container_name) container.stop() print(f"Container {selected_container_name} stopped successfully.") + except docker.errors.DockerException as e: + print(f"Error: Unable to connect to Docker daemon: {e}", file=sys.stderr) + sys.exit(1) except docker.errors.NotFound: print(f"Error: Container {selected_container_name} not found.") except docker.errors.APIError as e: diff --git a/arm_cli/self/self.py b/arm_cli/self/self.py index 96d4520..45327b4 100644 --- a/arm_cli/self/self.py +++ b/arm_cli/self/self.py @@ -54,8 +54,45 @@ def update(ctx, source, force): safe_run(["python", "-c", "import importlib; importlib.invalidate_caches()"]) # Install from the provided source path - safe_run([sys.executable, "-m", "pip", "install", "-e", source], check=True) - print(f"arm-cli installed from source at {source} successfully!") + try: + # Prefer editable install for developer workflows + safe_run([sys.executable, "-m", "pip", "install", "-e", source], check=True) + print(f"arm-cli installed from source (editable) at {source} successfully!") + except subprocess.CalledProcessError: + # Any failure on editable install: fall back to a standard install + print("Editable install failed. Falling back to a standard install...") + try: + safe_run([sys.executable, "-m", "pip", "install", source], check=True) + print(f"arm-cli installed from source (standard) at {source} successfully!") + except subprocess.CalledProcessError: + # Provide guidance without mutating the user's environment + print( + "Standard install also failed. This is typically due to outdated build tooling " + "being pulled during build isolation (e.g., old setuptools).", + file=sys.stderr, + ) + print( + "You can resolve this by upgrading build tools, then retrying:", + file=sys.stderr, + ) + print( + " python -m pip install --upgrade pip setuptools wheel build setuptools-scm", + file=sys.stderr, + ) + print( + " python -m pip install . # or: python -m pip install -e .", + file=sys.stderr, + ) + print( + "Alternatively, if you already upgraded tools in this environment, you can bypass " + "build isolation:", + file=sys.stderr, + ) + print( + " PIP_NO_BUILD_ISOLATION=1 python -m pip install .", + file=sys.stderr, + ) + raise else: print("Updating arm-cli from PyPI...") diff --git a/arm_cli/system/setup_utils.py b/arm_cli/system/setup_utils.py index 4008d61..f3b93e3 100644 --- a/arm_cli/system/setup_utils.py +++ b/arm_cli/system/setup_utils.py @@ -1,4 +1,5 @@ import os +import shutil import stat import subprocess import sys @@ -263,7 +264,79 @@ def setup_shell(force=False): bashrc_path = os.path.expanduser("~/.bashrc") line = f"source {get_current_shell_addins()}" - if not is_line_in_file(line, bashrc_path): + # Detect any existing sourcing of shell_addins.sh (even from old paths) + existing_lines = [] + try: + with open(bashrc_path, "r") as f: + for file_line in f: + stripped = file_line.strip() + if ( + stripped.startswith("source") + and "/arm_cli/system/shell_scripts/shell_addins.sh" in stripped + ): + existing_lines.append(stripped) + except FileNotFoundError: + existing_lines = [] + + # If there are old references that don't match the current path, warn the user + outdated_lines = [old_line for old_line in existing_lines if old_line != line] + if outdated_lines: + click.secho( + "Warning: Found old shell addins entries in ~/.bashrc that reference a previous Python/site-packages path.", + fg="yellow", + err=True, + ) + for old in outdated_lines: + click.secho(f" - {old}", fg="yellow", err=True) + click.secho( + "It is recommended to remove these old lines to avoid duplicate sourcing.", + fg="yellow", + err=True, + ) + + # Offer to update outdated entries in-place to the current path + if force or click.confirm( + "Do you want me to update these outdated entries to the current path now? " + "A backup will be saved to /tmp/.bashrc_backup." + ): + try: + # Backup current ~/.bashrc + shutil.copyfile(bashrc_path, "/tmp/.bashrc_backup") + with open(bashrc_path, "r") as f: + contents = f.readlines() + new_contents = [] + for file_line in contents: + if ( + file_line.strip().startswith("source") + and "/arm_cli/system/shell_scripts/shell_addins.sh" in file_line + and file_line.strip() != line + ): + new_contents.append(line + "\n") + else: + new_contents.append(file_line) + with open(bashrc_path, "w") as f: + f.writelines(new_contents) + # Refresh existing_lines state after modification + existing_lines = [] + for file_line in new_contents: + stripped = file_line.strip() + if ( + stripped.startswith("source") + and "/arm_cli/system/shell_scripts/shell_addins.sh" in stripped + ): + existing_lines.append(stripped) + print( + "Updated outdated shell addins entries in ~/.bashrc (backup at /tmp/.bashrc_backup)" + ) + except Exception as e: + click.secho( + f"Failed to update ~/.bashrc automatically: {e}", + fg="yellow", + err=True, + ) + + # If the current line is not present, append it + if line not in existing_lines: print(f'Adding \n"{line}"\nto {bashrc_path}') if not force: if not click.confirm("Do you want to add shell autocomplete to ~/.bashrc?"): diff --git a/pyproject.toml b/pyproject.toml index fe82585..5fd04c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ dependencies = [ "beartype", "click", "click-completion", - "docker", + "docker>=7.0.0", "inquirer", "pydantic", ]