mac-upkeep 2.4.0__tar.gz → 2.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mac_upkeep-2.4.2/.release-please-manifest.json +3 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/CHANGELOG.md +14 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/CLAUDE.md +1 -1
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/PKG-INFO +4 -4
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/README.md +3 -3
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/pyproject.toml +1 -1
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/src/mac_upkeep/defaults.toml +4 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/src/mac_upkeep/git_sync.py +23 -9
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/tests/test_config.py +1 -1
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/tests/test_git_sync.py +25 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/uv.lock +1 -1
- mac_upkeep-2.4.0/.release-please-manifest.json +0 -3
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/.github/workflows/release.yml +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/.github/workflows/test.yml +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/.gitignore +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/CONTRIBUTING.md +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/LICENSE +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/demo/demo.gif +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/demo/record.sh +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/docs/reusable-patterns.md +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/llms.txt +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/release-please-config.json +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/src/mac_upkeep/__init__.py +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/src/mac_upkeep/cli.py +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/src/mac_upkeep/config.py +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/src/mac_upkeep/notify.py +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/src/mac_upkeep/output.py +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/src/mac_upkeep/py.typed +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/src/mac_upkeep/tasks.py +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/tests/__init__.py +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/tests/test_cli.py +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/tests/test_notify.py +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/tests/test_output.py +0 -0
- {mac_upkeep-2.4.0 → mac_upkeep-2.4.2}/tests/test_tasks.py +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.4.2](https://github.com/calvindotsg/mac-upkeep/compare/v2.4.1...v2.4.2) (2026-06-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* git_sync timeout resilience + disable broken pnpm store-prune ([#39](https://github.com/calvindotsg/mac-upkeep/issues/39)) ([c9b2665](https://github.com/calvindotsg/mac-upkeep/commit/c9b2665cb9be85d4f49b39aa967ab001473f5b22))
|
|
9
|
+
|
|
10
|
+
## [2.4.1](https://github.com/calvindotsg/mac-upkeep/compare/v2.4.0...v2.4.1) (2026-04-22)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* correct Mole link in README to tw93/Mole ([#37](https://github.com/calvindotsg/mac-upkeep/issues/37)) ([6f0b1ff](https://github.com/calvindotsg/mac-upkeep/commit/6f0b1ff5ba107ab9b9aab0242c823e5aaf4e1a08))
|
|
16
|
+
|
|
3
17
|
## [2.4.0](https://github.com/calvindotsg/mac-upkeep/compare/v2.3.0...v2.4.0) (2026-04-20)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -103,7 +103,7 @@ A TaskDef with `handler="<name>"` and empty `command` bypasses subprocess buildi
|
|
|
103
103
|
- **Do not open terminal windows from launchd** — fragile (focus stealing, macOS 13+ permission escalation). Use headless + notification + click-to-act.
|
|
104
104
|
- **Testing**: `Config.load()` calls `get_brew_prefix()` which runs `subprocess.run(["brew", "--prefix"])`. Tests that mock `subprocess.run` or `shutil.which` will capture this call too (shared module objects). Mock `mac_upkeep.config.get_brew_prefix` directly in `init` command tests.
|
|
105
105
|
- **`git_sync` SSH auth under launchd** relies on `~/.ssh/config` `IdentityAgent` (path-based, e.g. 1Password socket). `SSH_AUTH_SOCK` env vars are NOT inherited by LaunchAgents, so env-based agent forwarding won't work here — the `IdentityAgent` directive is the supported path.
|
|
106
|
-
- **`git_sync` forces `GIT_TERMINAL_PROMPT=0` and defaults `GIT_ASKPASS=/usr/bin/true`** (user-set `GIT_ASKPASS` is respected) — fail-fast on auth misconfiguration instead of stalling to the 60 s subprocess timeout.
|
|
106
|
+
- **`git_sync` forces `GIT_TERMINAL_PROMPT=0` and defaults `GIT_ASKPASS=/usr/bin/true`** (user-set `GIT_ASKPASS` is respected) — fail-fast on auth misconfiguration instead of stalling to the 60 s subprocess timeout. A genuine stall (network/server hang) still hits the timeout, so `_run_git` catches `subprocess.TimeoutExpired` and returns a synthetic `CompletedProcess(returncode=124, stderr="timed out after {timeout}s")`. That marks only the affected repo failed and lets the loop continue — a hung pull cannot abort the whole run (which would otherwise skip `output.summary()`/`notify()` and later tasks).
|
|
107
107
|
|
|
108
108
|
## Release Process
|
|
109
109
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mac-upkeep
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: Automated macOS maintenance CLI
|
|
5
5
|
Project-URL: Homepage, https://github.com/calvindotsg/mac-upkeep
|
|
6
6
|
Project-URL: Repository, https://github.com/calvindotsg/mac-upkeep
|
|
@@ -61,9 +61,9 @@ uvx mac-upkeep run # one-off without installing
|
|
|
61
61
|
| `pnpm` | Prune pnpm content-addressable store | Monthly |
|
|
62
62
|
| `uv` | Prune uv package cache | Monthly |
|
|
63
63
|
| `fisher` | Update Fish shell plugins | Weekly |
|
|
64
|
-
| `mo_clean` | Clean system and user caches ([
|
|
65
|
-
| `mo_optimize` | Optimize DNS, Spotlight, fonts, Dock ([
|
|
66
|
-
| `mo_purge` | Remove old project artifacts ([
|
|
64
|
+
| `mo_clean` | Clean system and user caches ([Mole](https://github.com/tw93/Mole)) | Weekly |
|
|
65
|
+
| `mo_optimize` | Optimize DNS, Spotlight, fonts, Dock ([Mole](https://github.com/tw93/Mole)) | Weekly |
|
|
66
|
+
| `mo_purge` | Remove old project artifacts ([Mole](https://github.com/tw93/Mole)) | Monthly |
|
|
67
67
|
| `brew_cleanup` | Remove old versions and cache files | Monthly |
|
|
68
68
|
| `brew_bundle` | Remove packages not in Brewfile | Weekly |
|
|
69
69
|
| `git_sync` | Pull configured git repositories | Daily |
|
|
@@ -34,9 +34,9 @@ uvx mac-upkeep run # one-off without installing
|
|
|
34
34
|
| `pnpm` | Prune pnpm content-addressable store | Monthly |
|
|
35
35
|
| `uv` | Prune uv package cache | Monthly |
|
|
36
36
|
| `fisher` | Update Fish shell plugins | Weekly |
|
|
37
|
-
| `mo_clean` | Clean system and user caches ([
|
|
38
|
-
| `mo_optimize` | Optimize DNS, Spotlight, fonts, Dock ([
|
|
39
|
-
| `mo_purge` | Remove old project artifacts ([
|
|
37
|
+
| `mo_clean` | Clean system and user caches ([Mole](https://github.com/tw93/Mole)) | Weekly |
|
|
38
|
+
| `mo_optimize` | Optimize DNS, Spotlight, fonts, Dock ([Mole](https://github.com/tw93/Mole)) | Weekly |
|
|
39
|
+
| `mo_purge` | Remove old project artifacts ([Mole](https://github.com/tw93/Mole)) | Monthly |
|
|
40
40
|
| `brew_cleanup` | Remove old versions and cache files | Monthly |
|
|
41
41
|
| `brew_bundle` | Remove packages not in Brewfile | Weekly |
|
|
42
42
|
| `git_sync` | Pull configured git repositories | Daily |
|
|
@@ -22,6 +22,10 @@ description = "Prune pnpm content-addressable store"
|
|
|
22
22
|
command = "pnpm store prune"
|
|
23
23
|
detect = "pnpm"
|
|
24
24
|
frequency = "monthly"
|
|
25
|
+
# Disabled by default: on pnpm 11 `store prune` corrupts the global virtual
|
|
26
|
+
# store, removing in-use packages (pnpm/pnpm#10132). Re-enable per-machine via
|
|
27
|
+
# `[tasks.pnpm]\nenabled = true` once upstream is fixed.
|
|
28
|
+
enabled = false
|
|
25
29
|
|
|
26
30
|
[tasks.uv]
|
|
27
31
|
description = "Prune uv package cache"
|
|
@@ -29,14 +29,23 @@ def _build_env() -> dict[str, str]:
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def _run_git(path: str, args: list[str], *, timeout: int = 60) -> subprocess.CompletedProcess[str]:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
cmd = ["git", "-C", path, *args]
|
|
33
|
+
try:
|
|
34
|
+
return subprocess.run(
|
|
35
|
+
cmd,
|
|
36
|
+
capture_output=True,
|
|
37
|
+
text=True,
|
|
38
|
+
timeout=timeout,
|
|
39
|
+
stdin=subprocess.DEVNULL,
|
|
40
|
+
env=_build_env(),
|
|
41
|
+
)
|
|
42
|
+
except subprocess.TimeoutExpired:
|
|
43
|
+
# A hung git call (e.g. network/auth stall on pull) must not abort the
|
|
44
|
+
# whole run. Return a synthetic failure so _sync_repo marks this repo
|
|
45
|
+
# failed and continues. 124 mirrors GNU timeout's convention.
|
|
46
|
+
return subprocess.CompletedProcess(
|
|
47
|
+
cmd, returncode=124, stdout="", stderr=f"timed out after {timeout}s"
|
|
48
|
+
)
|
|
40
49
|
|
|
41
50
|
|
|
42
51
|
def _resolve_paths(patterns: list[str], output: Output) -> list[str]:
|
|
@@ -62,7 +71,12 @@ def _resolve_paths(patterns: list[str], output: Output) -> list[str]:
|
|
|
62
71
|
|
|
63
72
|
|
|
64
73
|
def _sync_repo(path: str, *, skip_dirty: bool) -> tuple[str, str]:
|
|
65
|
-
"""Sync one repo. Returns (status, reason) where status is pulled|up-to-date|skipped|failed.
|
|
74
|
+
"""Sync one repo. Returns (status, reason) where status is pulled|up-to-date|skipped|failed.
|
|
75
|
+
|
|
76
|
+
Only `pull` realistically times out (it's the sole network call); a 124 timeout on
|
|
77
|
+
the local pre-pull checks below would fall through to their "skipped" branches, which
|
|
78
|
+
is fine — the run still completes either way thanks to _run_git's TimeoutExpired catch.
|
|
79
|
+
"""
|
|
66
80
|
r = _run_git(path, ["rev-parse", "--is-inside-work-tree"])
|
|
67
81
|
if r.returncode != 0:
|
|
68
82
|
return "skipped", "not a git repo"
|
|
@@ -394,7 +394,7 @@ def test_env_var_disables_task(monkeypatch):
|
|
|
394
394
|
monkeypatch.setenv("MAC_UPKEEP_GCLOUD", "false")
|
|
395
395
|
config = Config.load(Path("/nonexistent/config.toml"))
|
|
396
396
|
assert not config.is_enabled("gcloud")
|
|
397
|
-
assert config.is_enabled("
|
|
397
|
+
assert config.is_enabled("uv") # unrelated task stays enabled
|
|
398
398
|
|
|
399
399
|
|
|
400
400
|
def test_env_var_overrides_config(tmp_path):
|
|
@@ -277,3 +277,28 @@ def test_failure_surfaces_basename_and_stderr(tmp_path, monkeypatch):
|
|
|
277
277
|
assert result.status == "failed"
|
|
278
278
|
assert "1 failed: biz" == result.reason
|
|
279
279
|
assert any("Permission denied" in c[0][0] for c in output.task_debug.call_args_list)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_pull_timeout_does_not_crash_run(tmp_path, monkeypatch):
|
|
283
|
+
"""A hung pull raises TimeoutExpired inside subprocess.run; it must be caught
|
|
284
|
+
and surfaced as a failed repo rather than aborting the whole run."""
|
|
285
|
+
p = _make_repo(tmp_path, "biz")
|
|
286
|
+
config = _config([p])
|
|
287
|
+
output = MagicMock()
|
|
288
|
+
# MagicMock raises any exception instance found in side_effect, so the pull
|
|
289
|
+
# step (last) raises TimeoutExpired exactly where _run_git calls subprocess.run.
|
|
290
|
+
run_mock = MagicMock(
|
|
291
|
+
side_effect=[
|
|
292
|
+
_cp(returncode=0, stdout="true\n"),
|
|
293
|
+
_cp(returncode=0, stdout="origin\n"),
|
|
294
|
+
_cp(returncode=0, stdout="main\n"),
|
|
295
|
+
_cp(returncode=0, stdout="origin/main\n"),
|
|
296
|
+
_cp(returncode=0, stdout=""), # clean worktree
|
|
297
|
+
subprocess.TimeoutExpired(cmd=["git", "pull"], timeout=60), # pull hangs
|
|
298
|
+
]
|
|
299
|
+
)
|
|
300
|
+
monkeypatch.setattr("mac_upkeep.git_sync.subprocess.run", run_mock)
|
|
301
|
+
result = run_git_sync(config, output, dry_run=False)
|
|
302
|
+
assert result.status == "failed"
|
|
303
|
+
assert "1 failed: biz" == result.reason
|
|
304
|
+
assert any("timed out after 60s" in c[0][0] for c in output.task_debug.call_args_list)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|