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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 25 additions & 2 deletions arm_cli/container/container.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import subprocess
import sys

import click
import docker
Expand All @@ -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")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
41 changes: 39 additions & 2 deletions arm_cli/self/self.py
Original file line number Diff line number Diff line change
Expand Up @@ -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...")

Expand Down
75 changes: 74 additions & 1 deletion arm_cli/system/setup_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import shutil
import stat
import subprocess
import sys
Expand Down Expand Up @@ -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?"):
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ dependencies = [
"beartype",
"click",
"click-completion",
"docker",
"docker>=7.0.0",
"inquirer",
"pydantic",
]
Expand Down