Skip to content

Commit a446e90

Browse files
authored
Merge pull request #5 from barisgit/feature/self-update
Feature/self update
2 parents 9504760 + 010d657 commit a446e90

File tree

22 files changed

+1230
-473
lines changed

22 files changed

+1230
-473
lines changed

.pre-commit-config.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
repos:
2+
# Ruff formatter
3+
- repo: https://github.com/astral-sh/ruff-pre-commit
4+
rev: v0.11.6
5+
hooks:
6+
- id: ruff-format
7+
name: ruff format
8+
description: Format Python code with ruff
9+
10+
# Ruff linter
11+
- repo: https://github.com/astral-sh/ruff-pre-commit
12+
rev: v0.11.6
13+
hooks:
14+
- id: ruff
15+
name: ruff check --fix
16+
description: Lint Python code with ruff and auto-fix issues
17+
args: [--fix]
18+
19+
# Pyrefly type checker
20+
- repo: local
21+
hooks:
22+
- id: pyrefly
23+
name: pyrefly check
24+
description: Type check Python code with pyrefly
25+
entry: .venv/bin/pyrefly
26+
args: [check]
27+
language: system
28+
types: [python]
29+
30+
# Pytest tests
31+
- repo: local
32+
hooks:
33+
- id: pytest
34+
name: pytest
35+
description: Run tests with pytest
36+
entry: .venv/bin/pytest
37+
language: system
38+
types: [python]
39+
pass_filenames: false
40+
always_run: true

docs/astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export default defineConfig({
6060
{ label: "template", link: "/reference/cli/template/" },
6161
{ label: "list", link: "/reference/cli/list/" },
6262
{ label: "status", link: "/reference/cli/status/" },
63+
{ label: "sync", link: "/reference/cli/sync/" },
6364
{ label: "update", link: "/reference/cli/update/" },
6465
{ label: "add-hook", link: "/reference/cli/add-hook/" },
6566
],

docs/src/content/docs/guides/automatic-updates.mdx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Aside } from "@astrojs/starlight/components";
77

88
> **Note:** This guide primarily describes features beneficial for **Consumers** – those using libraries managed by a Creator. The setup of the hook itself might be done by a Creator or an advanced Consumer.
99
10-
KiLM provides a helper command [`kilm add-hook`](/reference/cli/add-hook/) to easily create a basic `post-merge` Git hook that runs `kilm update`.
10+
KiLM provides a helper command [`kilm add-hook`](/reference/cli/add-hook/) to easily create a basic `post-merge` Git hook that runs `kilm sync`.
1111

1212
## Using `kilm add-hook` (Recommended)
1313

@@ -17,7 +17,7 @@ Navigate to the root of your Git repository containing your KiCad libraries and
1717
kilm add-hook
1818
```
1919

20-
This creates a `.git/hooks/post-merge` script that automatically executes `kilm update` every time you successfully run `git pull` or `git merge` in that repository.
20+
This creates a `.git/hooks/post-merge` script that automatically executes `kilm sync` every time you successfully run `git pull` or `git merge` in that repository.
2121

2222
<Aside type="tip" title="Enhanced Hook Management">
2323
The `kilm add-hook` command now includes advanced features: - **Smart
@@ -51,13 +51,13 @@ If you prefer manual setup or want to customize the hook script further, you can
5151
# KiCad Library Manager auto-update hook
5252
# Added manually
5353

54-
echo "Running KiCad Library Manager update..."
55-
kilm update
54+
echo "Running KiCad Library Manager sync..."
55+
kilm sync
5656

5757
# Uncomment to set up libraries automatically (use with caution)
5858
# kilm setup
5959

60-
echo "KiCad libraries update complete."
60+
echo "KiCad libraries sync complete."
6161
# END KiLM-managed section
6262
```
6363

@@ -72,7 +72,7 @@ If you prefer manual setup or want to customize the hook script further, you can
7272
When you run `git pull` or `git merge`:
7373

7474
1. Git executes the `post-merge` hook automatically
75-
2. The hook runs `kilm update` to check for library updates
75+
2. The hook runs `kilm sync` to check for library updates
7676
3. If new versions are available, they are pulled from remote repositories
7777
4. Your local KiCad libraries stay synchronized with the latest versions
7878

docs/src/content/docs/guides/troubleshooting.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,6 @@ Here are some common problems you might encounter when using KiLM and how to res
5050

5151
## Getting More Help
5252

53-
- Use the `--verbose` flag with commands like `kilm setup` or `kilm update` to get more detailed output.
53+
- Use the `--verbose` flag with commands like `kilm setup` or `kilm sync` to get more detailed output.
5454
- Use `kilm status` to check the current configuration state.
5555
- Check the KiLM issue tracker on GitHub: [https://github.com/barisgit/kilm/issues](https://github.com/barisgit/kilm/issues) or submit a request here: [https://aristovnik.me/contact](https://aristovnik.me/contact)

docs/src/content/docs/reference/cli/index.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ Each command focuses on a specific task. Below is a list of available commands.
1818
- [`template`](/reference/cli/template/): Create and manage KiCad project templates.
1919
- [`list`](/reference/cli/list/): List symbol and footprint libraries found within a specified directory.
2020
- [`status`](/reference/cli/status/): Check the current KiLM and KiCad configuration status.
21-
- [`update`](/reference/cli/update/): Update Git-based libraries using `git pull`.
22-
- [`add-hook`](/reference/cli/add-hook/): Add a Git post-merge hook to automatically run `kilm update`.
21+
- [`sync`](/reference/cli/sync/): Update Git-based libraries using `git pull`.
22+
- [`update`](/reference/cli/update/): Update KiLM itself to the latest version from PyPI.
23+
- [`add-hook`](/reference/cli/add-hook/): Add a Git post-merge hook to automatically run `kilm sync`.
2324

2425
Use the sidebar navigation or the links above to explore each command in detail.

kicad_lib_manager/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
"""
22
KiCad Library Manager - Library management utilities for KiCad
33
"""
4+
5+
__version__ = "0.4.0"

kicad_lib_manager/auto_update.py

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
"""
2+
Auto-update functionality for KiLM.
3+
Handles installation method detection, PyPI integration, and update execution.
4+
"""
5+
6+
import json
7+
import os
8+
import subprocess
9+
import sys
10+
import time
11+
from pathlib import Path
12+
from typing import Dict, Optional, Tuple
13+
14+
import requests
15+
from packaging.version import InvalidVersion, Version
16+
17+
18+
def detect_installation_method() -> str:
19+
"""
20+
Detect how KiLM was installed to determine appropriate update strategy.
21+
Returns: 'pipx' | 'pip' | 'pip-venv' | 'uv' | 'conda'
22+
"""
23+
executable_path = Path(sys.executable)
24+
25+
# Check for pipx installation
26+
if any(
27+
part in str(executable_path) for part in [".local/share/pipx", "pipx/venvs"]
28+
):
29+
return "pipx"
30+
31+
if os.environ.get("PIPX_HOME") and "pipx" in str(executable_path):
32+
return "pipx"
33+
34+
# Check for conda installation
35+
if os.environ.get("CONDA_DEFAULT_ENV") or "conda" in str(executable_path):
36+
return "conda"
37+
38+
# Check for uv installation (only via official environment variables)
39+
if os.environ.get("UV_TOOL_DIR") or os.environ.get("UV_TOOL_BIN_DIR"):
40+
return "uv"
41+
42+
# Check for virtual environment (pip in venv)
43+
if os.environ.get("VIRTUAL_ENV") or sys.prefix != getattr(
44+
sys, "base_prefix", sys.prefix
45+
):
46+
return "pip-venv"
47+
48+
# Check for homebrew installation (strict path check)
49+
if str(executable_path).startswith(("/opt/homebrew/", "/usr/local/Cellar/")):
50+
return "homebrew"
51+
52+
# Default to system pip
53+
return "pip"
54+
55+
56+
class PyPIVersionChecker:
57+
"""Responsible PyPI API client with caching and proper headers."""
58+
59+
def __init__(self, package_name: str, version: str = "unknown"):
60+
self.package_name = package_name
61+
self.base_url = f"https://pypi.org/pypi/{package_name}/json"
62+
self.cache_file = Path.home() / ".cache" / "kilm" / "version_check.json"
63+
self.user_agent = f"KiLM/{version} (+https://github.com/barisgit/KiLM)"
64+
65+
def check_latest_version(self) -> Optional[str]:
66+
"""
67+
Check latest version from PyPI with caching and rate limiting.
68+
Returns None if check fails or is rate limited.
69+
"""
70+
try:
71+
headers = {"User-Agent": self.user_agent}
72+
73+
# Use cached ETag if available
74+
cached_data = self._load_cache()
75+
if cached_data is not None and "etag" in cached_data:
76+
headers["If-None-Match"] = cached_data["etag"]
77+
78+
response = requests.get(self.base_url, headers=headers, timeout=10)
79+
80+
if response.status_code == 304: # Not Modified
81+
return cached_data.get("version") if cached_data else None
82+
83+
if response.status_code == 200:
84+
data = response.json()
85+
latest_version = data["info"]["version"]
86+
87+
# Cache response with ETag
88+
self._save_cache(
89+
{
90+
"version": latest_version,
91+
"etag": response.headers.get("ETag"),
92+
"timestamp": time.time(),
93+
}
94+
)
95+
96+
return latest_version
97+
98+
except (requests.RequestException, KeyError, json.JSONDecodeError):
99+
# Fail silently - don't block CLI functionality
100+
pass
101+
102+
return None
103+
104+
def _load_cache(self) -> Optional[Dict]:
105+
"""Load cached version data."""
106+
if self.cache_file.exists():
107+
try:
108+
with Path(self.cache_file).open() as f:
109+
data = json.load(f)
110+
# Cache valid for 24 hours
111+
if time.time() - data.get("timestamp", 0) < 86400:
112+
return data
113+
except (json.JSONDecodeError, KeyError):
114+
pass
115+
return None
116+
117+
def _save_cache(self, data: Dict):
118+
"""Save version data to cache."""
119+
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
120+
with Path(self.cache_file).open("w") as f:
121+
json.dump(data, f)
122+
123+
124+
def update_via_pipx() -> bool:
125+
"""Update KiLM via pipx. Most reliable method for CLI tools."""
126+
try:
127+
result = subprocess.run(
128+
["pipx", "upgrade", "kilm"], capture_output=True, text=True, timeout=300
129+
)
130+
return result.returncode == 0
131+
except (subprocess.TimeoutExpired, FileNotFoundError):
132+
return False
133+
134+
135+
def update_via_pip() -> bool:
136+
"""Update KiLM via pip."""
137+
try:
138+
# Use same Python interpreter that's running KiLM
139+
result = subprocess.run(
140+
[sys.executable, "-m", "pip", "install", "--upgrade", "kilm"],
141+
capture_output=True,
142+
text=True,
143+
timeout=300,
144+
)
145+
return result.returncode == 0
146+
except (subprocess.TimeoutExpired, FileNotFoundError):
147+
return False
148+
149+
150+
def update_via_uv() -> bool:
151+
"""Update KiLM via uv."""
152+
try:
153+
result = subprocess.run(
154+
["uv", "tool", "upgrade", "kilm"],
155+
capture_output=True,
156+
text=True,
157+
timeout=300,
158+
)
159+
return result.returncode == 0
160+
except (subprocess.TimeoutExpired, FileNotFoundError):
161+
return False
162+
163+
164+
class UpdateManager:
165+
"""Manages update checking and execution for KiLM."""
166+
167+
def __init__(self, current_version: str):
168+
self.version_checker = PyPIVersionChecker("kilm")
169+
self.current_version = current_version
170+
self.installation_method = detect_installation_method()
171+
172+
def check_latest_version(self) -> Optional[str]:
173+
"""Check for latest version available on PyPI."""
174+
return self.version_checker.check_latest_version()
175+
176+
def is_newer_version_available(self, latest_version: str) -> bool:
177+
"""Compare versions to determine if update is available."""
178+
try:
179+
current_ver = Version(self.current_version)
180+
latest_ver = Version(latest_version)
181+
return latest_ver > current_ver
182+
except (InvalidVersion, AttributeError):
183+
return False
184+
185+
def get_update_instruction(self) -> str:
186+
"""Get update instruction for the detected installation method."""
187+
instructions = {
188+
"pipx": "pipx upgrade kilm",
189+
"pip": "pip install --upgrade kilm",
190+
"pip-venv": "pip install --upgrade kilm",
191+
"uv": "uv tool upgrade kilm",
192+
"conda": "Conda package not yet available (planned for future)",
193+
"homebrew": "Homebrew package not yet available (planned for future)",
194+
}
195+
return instructions.get(self.installation_method, "Check your package manager")
196+
197+
def can_auto_update(self) -> bool:
198+
"""Check if automatic update is possible for this installation method."""
199+
return self.installation_method in ["pipx", "pip", "pip-venv", "uv"]
200+
201+
def perform_update(self) -> Tuple[bool, str]:
202+
"""
203+
Execute update using detected installation method.
204+
Returns: (success: bool, message: str)
205+
"""
206+
if not self.can_auto_update():
207+
instruction = self.get_update_instruction()
208+
return False, f"Manual update required. Run: {instruction}"
209+
210+
update_functions = {
211+
"pipx": update_via_pipx,
212+
"pip": update_via_pip,
213+
"pip-venv": update_via_pip,
214+
"uv": update_via_uv,
215+
}
216+
217+
update_func = update_functions.get(self.installation_method)
218+
if update_func:
219+
try:
220+
success = update_func()
221+
if success:
222+
return True, "KiLM updated successfully!"
223+
else:
224+
instruction = self.get_update_instruction()
225+
return False, f"Auto-update failed. Try manually: {instruction}"
226+
except Exception as e:
227+
return False, f"Update error: {str(e)}"
228+
else:
229+
instruction = self.get_update_instruction()
230+
return False, f"Unsupported installation method. Run: {instruction}"

kicad_lib_manager/cli.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .commands.pin import pin
1616
from .commands.setup import setup
1717
from .commands.status import status
18+
from .commands.sync import sync
1819
from .commands.template import template
1920
from .commands.unpin import unpin
2021
from .commands.update import update
@@ -39,6 +40,7 @@ def main():
3940
main.add_command(init)
4041
main.add_command(add_3d)
4142
main.add_command(config)
43+
main.add_command(sync)
4244
main.add_command(update)
4345
main.add_command(add_hook)
4446
main.add_command(template)

kicad_lib_manager/commands/add_hook/command.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@
3030
show_default=True,
3131
)
3232
def add_hook(directory, force):
33-
"""Add a Git post-merge hook to automatically update KiCad libraries.
33+
"""Add a Git post-merge hook to automatically sync KiCad libraries.
3434
3535
This command adds a Git post-merge hook to the specified repository
3636
(or the current directory if none specified) that automatically runs
37-
'kilm update' after a 'git pull' or 'git merge' operation.
37+
'kilm sync' after a 'git pull' or 'git merge' operation.
3838
3939
This ensures your KiCad libraries are always up-to-date after pulling
4040
changes from remote repositories.
@@ -98,7 +98,7 @@ def add_hook(directory, force):
9898

9999
click.echo(f"Successfully installed post-merge hook at {post_merge_hook}")
100100
click.echo(
101-
"The hook will run 'kilm update' after every 'git pull' or 'git merge' operation."
101+
"The hook will run 'kilm sync' after every 'git pull' or 'git merge' operation."
102102
)
103103

104104
if post_merge_hook.exists() and "KiLM-managed section" in new_content:

0 commit comments

Comments
 (0)