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
34 changes: 14 additions & 20 deletions fixos/cli/_cleanup_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -839,7 +839,7 @@ def _toolbox_duplicate_installs(installs: list[dict] | None) -> list[str]:


def _jetbrains_candidates() -> list[dict]:
"""Stale Toolbox channels (all but newest per app), unused version data and IDE caches."""
"""Stale Toolbox channels and unused version data; preserve system caches."""
home = Path.home()
candidates = _jetbrains_stale_data_dirs(home, _toolbox_installs(home))
apps_dir = home / ".local/share/JetBrains/Toolbox/apps"
Expand All @@ -864,29 +864,23 @@ def _jetbrains_candidates() -> list[dict]:
"note": "stary kanał Toolbox",
}
)
cache_dir = home / ".cache/JetBrains"
if cache_dir.is_dir():
for entry in sorted(cache_dir.iterdir()):
if entry.is_symlink():
continue
candidates.append(
{
"description": entry.name,
"path": entry,
"size_bytes": _dir_size(entry),
"note": "cache IDE",
}
)
# IDE system roots mix rebuildable indexes with irreplaceable LocalHistory
# and recovery/Toolbox state. Never offer a whole root for bulk deletion.
candidates.sort(key=lambda item: -item["size_bytes"])
return candidates


def _cleanup_jetbrains(
json_output: bool, dry_run: bool, list_only: bool, yes: bool
) -> None:
"""Remove stale JetBrains Toolbox channels, unused version data and IDE caches."""
"""Remove stale Toolbox channels and unused version data; retain system caches."""
candidates = _jetbrains_candidates()
advisories = _toolbox_duplicate_installs(_toolbox_installs(Path.home()))
if (Path.home() / ".cache/JetBrains").is_dir():
advisories.append(
"Cache systemowy JetBrains jest chroniony: może zawierać "
"LocalHistory (lokalne wersje plików), dane odzyskiwania i stan Toolbox."
)
if json_output:
_echo_json(
{
Expand All @@ -906,10 +900,10 @@ def _cleanup_jetbrains(
click.echo(click.style(f" Uwaga: {advisory}", fg="yellow"))
if not candidates:
click.echo(
click.style("Brak starych kanałów Toolbox ani cache IDE.", fg="green")
click.style("Brak starych kanałów Toolbox ani nieużywanych danych wersji.", fg="green")
)
return
_display_candidates("JETBRAINS — stare kanały, dane starych wersji i cache:", candidates)
_display_candidates("JETBRAINS — stare kanały i dane starych wersji:", candidates)
if list_only:
return
selected = (
Expand Down Expand Up @@ -2004,14 +1998,14 @@ def _cleanup_thumbnails(
{
"id": "jetbrains",
"flag": "--jetbrains",
"name": "Stare IDE JetBrains i cache Toolbox",
"name": "Stare wersje IDE JetBrains",
"direct": True,
"default_retention": "stare wersje",
"default_days": 0.0,
"risk_level": "safe",
"default_safe": True,
"description": "Stare wersje IDE z Toolbox, nieużywane dane starszych wersji oraz ~/.cache/JetBrains",
"protected": "Dane wersji używanych przez instalacje z Toolbox state.json (także snap), ~/.config/JetBrains",
"description": "Stare wersje IDE z Toolbox i nieużywane dane starszych wersji",
"protected": "Dane wersji używanych przez instalacje z Toolbox state.json (także snap), ~/.config/JetBrains, ~/.cache/JetBrains (w tym LocalHistory)",
"example": "fixos cleanup --jetbrains",
},
{
Expand Down
6 changes: 3 additions & 3 deletions fixos/cli/cleanup_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -1951,8 +1951,8 @@ def _tier() -> TierResult:
is_flag=True,
default=False,
help=(
"Usuń stare wersje IDE z Toolbox (ch-*) i cache ~/.cache/JetBrains; "
"ustawienia IDE w ~/.local/share/JetBrains są chronione"
"Usuń stare wersje IDE z Toolbox (ch-*) i nieużywane dane wersji; "
"cache systemowy i LocalHistory w ~/.cache/JetBrains są chronione"
),
)
@click.option(
Expand Down Expand Up @@ -2182,7 +2182,7 @@ def cleanup_services(
fixos cleanup --snap-old # wyłączone rewizje snapów
fixos cleanup --journal # przycięcie dziennika systemd
fixos cleanup --user-cache --list # duże katalogi ~/.cache
fixos cleanup --jetbrains # stare IDE Toolbox + cache JetBrains
fixos cleanup --jetbrains # stare IDE Toolbox; cache i historia chronione
fixos cleanup --libvirt # obrazy VM niepodpięte pod domeny
fixos cleanup --gitive # stare izolowane workspace'y gitive
fixos cleanup --policy # przejrzysta tabela retencji i bezpośrednich opcji
Expand Down
35 changes: 35 additions & 0 deletions project/ticket-117/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Ticket 117: Preserve JetBrains system caches and Local History during explicit cleanup

- **ID**: ticket-117
- **Owner**: agent:codex
- **Status**: IN_PROGRESS
- **Workflow state**: PUBLICATION
- **Created**: 2026-10-31

## Goal and scope

The explicit --jetbrains cleanup path still selects whole IDE system-cache
roots, including LocalHistory, although generic service cleanup protects those
user file revisions. Exclude mixed system caches from this explicit path;
retain existing stale installation/version-data eligibility and explain the
protection in the existing CLI and JSON previews.

SESSION_EXECUTION_AUTHORIZATION: the user requested autonomous FixOS repairs
and execution of remaining host work on 2026-10-31. This safety correction
includes independent protected publication and both existing installations;
it does not authorize deleting live IDE data or user file history.

## Acceptance criteria

- [x] AC-01: Text/JSON previews do not propose entire JetBrains system-cache roots.
- [x] AC-02: Explicit --yes preserves cached user data and Local History while
existing stale Toolbox cleanup still works; regression and governance pass.
- [ ] AC-03: Full stack checks, independent exact-head publication, deployment
hashes, controlled history preservation and a live read-only preview pass.

## Validation

Three regression scenarios failed before the fix. All 58 affected tests and
the scoped Testless scan pass after the change. Changed-file Ruff and managed
governance pass. Full checks and independent publication bind the frozen head;
deployment verifies both installations and a read-only live preview.
76 changes: 76 additions & 0 deletions project/ticket-117/intent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
{
"schema": "new-project.intent/v3",
"ticket": "ticket-117",
"summary": "Preserve JetBrains system caches and Local History during explicit cleanup",
"workstream": "application",
"classification": {
"kind": "BUG",
"priority": "P1",
"origin": "requested"
},
"allowedPaths": [
"project/ticket-117/**",
"fixos/cli/_cleanup_space.py",
"fixos/cli/cleanup_cmd.py",
"tests/unit/test_cleanup_space.py"
],
"forbiddenPaths": [
"project/ticket-*/user-*.md"
],
"stacks": [],
"dependsOn": [],
"conflictsWith": [],
"integrationTicket": null,
"delivery": {
"acceptedBaseSha": "dc5de059fe93ddce373c048499976242abfaa167",
"targetBranch": "main",
"outcome": "Independently publish and deploy explicit JetBrains cleanup that preserves mixed system caches and irreplaceable Local History while retaining existing stale installation cleanup.",
"nonGoals": [
"No deletion of host IDE data or user history",
"No new cleanup category, package dependency, retention or storage threshold",
"No change to legacy stale installation eligibility"
],
"complexity": "S",
"estimatedMinutes": 10,
"budgets": {
"maxImplementationFiles": 3,
"maxAffectedComponents": 1,
"maxPublicInterfaceChanges": 0,
"maxRuntimeDependencies": 0
},
"architecture": {
"status": "accepted",
"decision": "Exclude whole JetBrains system-cache roots from explicit cleanup candidates and explain the protection in existing text/JSON previews and CLI help.",
"components": [
{
"name": "explicit-jetbrains-cleanup",
"paths": [
"fixos/cli/_cleanup_space.py",
"fixos/cli/cleanup_cmd.py",
"tests/unit/test_cleanup_space.py"
]
}
],
"responsibilityChanges": false,
"interfaceChanges": [],
"dataChanges": [],
"ui": {
"impact": "none",
"states": [],
"evidence": []
},
"rollback": "Revert through independent protected publication; no host cleanup is part of deployment."
},
"runtimeDependencies": [],
"validation": [
{
"criterion": "AC-01",
"commands": [
"python -m pytest tests/unit/test_cleanup_space.py -q",
"./project/governance-check.sh"
],
"evidence": "project/ticket-117/README.md"
}
]
}
}
48 changes: 47 additions & 1 deletion tests/unit/test_cleanup_space.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Tests for fixos.cli._cleanup_space disk-reclaim handlers."""

import json
import os
import shutil
import subprocess
import time
from pathlib import Path

import click
import pytest
from click.testing import CliRunner

from fixos.cli import _cleanup_space as space
Expand Down Expand Up @@ -186,7 +188,51 @@ def test_jetbrains_keeps_newest_toolbox_channel(monkeypatch, tmp_path):
assert "PyCharm ch-0" in result.output
assert "PyCharm ch-1" in result.output
assert "ch-2" not in result.output
assert "PyCharm2025.1" in result.output
assert "LocalHistory" in result.output


@pytest.mark.parametrize("cache_name", ["PyCharm2026.3", "unknown-system-data"])
def test_jetbrains_yes_preserves_system_cache_and_local_history(
monkeypatch, tmp_path, cache_name
):
home = tmp_path / "home"
apps = home / ".local/share/JetBrains/Toolbox/apps/PyCharm"
for channel in ("ch-0", "ch-1", "ch-2"):
(apps / channel).mkdir(parents=True)
history = home / ".cache/JetBrains" / cache_name / "LocalHistory/revisions.bin"
history.parent.mkdir(parents=True)
history.write_bytes(b"only local revision")
download = home / ".cache/JetBrains/Toolbox/download/installer.tar.gz"
download.parent.mkdir(parents=True)
download.write_bytes(b"toolbox state")
monkeypatch.setattr(Path, "home", classmethod(lambda cls: home))
monkeypatch.setattr(space, "_dir_size", lambda _path: 10)

result = _invoke(space._cleanup_jetbrains, False, False, False, True)

assert result.exit_code == 0, result.output
assert history.read_bytes() == b"only local revision"
assert download.read_bytes() == b"toolbox state"
assert not (apps / "ch-0").exists()
assert not (apps / "ch-1").exists()
assert (apps / "ch-2").is_dir()


def test_jetbrains_json_does_not_offer_bulk_system_cache(monkeypatch, tmp_path):
cache_root = tmp_path / ".cache/JetBrains"
(cache_root / "PyCharm2026.3/LocalHistory").mkdir(parents=True)
monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path))
monkeypatch.setattr(space, "_dir_size", lambda _path: 10)

result = _invoke(space._cleanup_jetbrains, True, False, False, True)

assert result.exit_code == 0, result.output
plan = json.loads(result.output)
assert not any(
Path(item["path"]).is_relative_to(cache_root)
for item in plan["candidates"]
)
assert "LocalHistory" in " ".join(plan["advisories"])


# ── libvirt ───────────────────────────────────────────────────────────────
Expand Down
Loading