Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/2251[#2251]: Provide generic uninstall support for globally installed tools (windows)
* https://github.com/devonfw/IDEasy/issues/1135[#1135]: Fix PowerShell env variable initialization on Windows by sourcing functions from the PowerShell profile
* https://github.com/devonfw/IDEasy/issues/741[#741]: Add a warning message for legacy devonfw-ide settings users
* https://github.com/devonfw/IDEasy/issues/2372[#2372]: Added `--retention-delay` option to `ide cleanup` to delete stale files in `updates`, `_ide/tmp` and `Downloads/ide`

The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/49?closed=1[milestone 2026.08.002].

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@

import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.commandlet.Commandlet;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.log.IdeLogLevel;
import com.devonfw.tools.ide.property.StringProperty;
import com.devonfw.tools.ide.step.Step;
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
import com.devonfw.tools.ide.tool.repository.ToolRepository;
Expand All @@ -21,6 +28,12 @@ public class CleanupCommandlet extends Commandlet {

private static final Logger LOG = LoggerFactory.getLogger(CleanupCommandlet.class);

/** The default retention period of stale files. Stale files are considered stale after 1 year (365 days) of inactivity. */
public static final Duration DEFAULT_RETENTION_DELAY = Duration.ofDays(365);

/** The {@link StringProperty} of the {@code --retention-delay} option. */
private final StringProperty retentionDelay;

/**
* Constructor.
*
Expand All @@ -30,6 +43,7 @@ public CleanupCommandlet(IdeContext context) {

super(context);
addKeyword(getName());
this.retentionDelay = add(new StringProperty("--retention-delay", false, null));
}

@Override
Expand All @@ -49,21 +63,59 @@ protected void doRun() {

LOG.debug("Start cleanup commandlet");

Duration retentionDelay = getRetentionDelay();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to rename this since you already have a StringProperty called retentionDelay in the class.


InstalledSoftware installedSoftware = new InstalledSoftware();

Step step = this.context.newStep("Identify unused software");
step.run(() -> discoverUnusedSoftware(installedSoftware), true);

logSoftwareToBeDeleted(installedSoftware.getTools());

if (hasSoftwareToDelete(installedSoftware.getTools())) {
List<Path> staleRoots = new ArrayList<>();
List<Path> staleFiles = new ArrayList<>();
if (this.context.getIdeHome() != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if-guard prevents any clean-up from happening, however _ide/tmp and the download cache cleanup would work when IDE_HOME is null. I'm not sure if this intentional, but you might want to change that if it's not.

Step staleStep = this.context.newStep("Identify stale files");
staleStep.run(() -> {
staleRoots.addAll(getStaleFileRoots());
discoverStaleFiles(staleRoots, staleFiles, retentionDelay);
}, true);
logStaleFilesToBeDeleted(staleFiles, retentionDelay);
}

boolean hasStaleFiles = !staleFiles.isEmpty();
if (hasSoftwareToDelete(installedSoftware.getTools()) || hasStaleFiles) {
this.context.askToContinue("Do you want to continue?");
deleteUnusedSoftware(installedSoftware.getTools());
if (hasStaleFiles) {
deleteStaleFiles(staleFiles, staleRoots);
}
}

LOG.debug("Finished cleanup commandlet");
}

/**
* Determines the retention delay to use.
*
* @return the retention delay, {@link #DEFAULT_RETENTION_DELAY} if the {@code --retention-delay} option was not provided.
* @throws CliException if the provided value is not a valid ISO-8601 time-based duration.
*/
private Duration getRetentionDelay() {

String value = this.retentionDelay.getValueAsString();
if (value == null) {
return DEFAULT_RETENTION_DELAY;
}
try {
return Duration.parse(value);
} catch (DateTimeParseException e) {
throw new CliException(
"Invalid value '" + value + "' for --retention-delay. Please provide a time-based ISO-8601 duration such as P30D or PT2H30M.",
e);
}
}
Comment on lines +104 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duration.parse() legally accepts negative and zero values. This means it won't throw the CliException and later down the line in isStale() every file under all roots becomes stale, which leads to every file being deleted. You should add a positivity check here.


/**
* Discovers installed and unused software.
*
Expand Down Expand Up @@ -356,4 +408,148 @@ private int deleteFolder(Path path) {
}
return 0;
}

/**
* Discovers stale files, i.e. files that have not been modified within the given retention delay, in the given root folders.
*
* @param roots the folders to scan.
* @param staleFiles the list to populate with the stale files.
* @param retentionDelay the age after which a file is considered stale.
*/
private void discoverStaleFiles(List<Path> roots, List<Path> staleFiles, Duration retentionDelay) {

for (Path root : roots) {
discoverStaleFilesRecursive(root, retentionDelay, staleFiles);
}
}

/**
* Recursively collects the stale files below the given folder, i.e. all files that are older than the retention delay.
*
* @param folder the folder to scan.
* @param retentionDelay the age after which a file is considered stale.
* @param staleFiles the list to populate with the stale files.
*/
private void discoverStaleFilesRecursive(Path folder, Duration retentionDelay, List<Path> staleFiles) {

if (!Files.isDirectory(folder)) {
return;
}

for (Path child : this.context.getFileAccess().listChildren(folder, child -> true)) {
if (Files.isDirectory(child)) {
discoverStaleFilesRecursive(child, retentionDelay, staleFiles);
} else if (isStale(child, retentionDelay)) {
staleFiles.add(child);
}
}
}
Comment on lines +433 to +446

@samuelkos17 samuelkos17 Aug 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files.isDirectory(child) follows links. If a symlink inside any root (e.g. ~/Downloads/ide/work -> C:\data) makes the scan descend outside the roots and delete the target's stale files, which would be a huge problem. Furthermore a self-referencing link causes unbounded recursion. You need to check for link children here and skip these. This also destroyed my IDEasy installation and I'm not sure how yours didn't get destroyed when you tested your work.


/**
* Determines the folders scanned for stale files: the IDEasy updates folder, the temporary folder, the download cache and the legacy download
* cache under {@code ~/Downloads/ide}.
*
* @return the list of root folders to scan, excluding folders that do not exist.
*/
private List<Path> getStaleFileRoots() {

List<Path> roots = new ArrayList<>();

addStaleFileRoot(roots, this.context.getIdeHome().resolve(IdeContext.FOLDER_UPDATES));
addStaleFileRoot(roots, this.context.getTempPath());
Path downloadPath = this.context.getDownloadPath();
addStaleFileRoot(roots, downloadPath);

// older versions kept the download cache under ~/Downloads/ide - scan that location too if it is distinct and still exists
Path legacy = this.context.getUserHome().resolve(IdeContext.FOLDER_DOWNLOADS).resolve("ide");
if (!legacy.equals(downloadPath)) {
addStaleFileRoot(roots, legacy);
}

return roots;
}

/**
* Adds the given folder to the list of scanned roots if it exists.
*
* @param roots the list of root folders to populate.
* @param root the candidate root folder.
*/
private void addStaleFileRoot(List<Path> roots, Path root) {

if (root != null && Files.exists(root)) {
roots.add(root);
}
}

/**
* Determines whether the given file is older than the given retention delay.
*
* @param file the file to check.
* @param retentionDelay the age after which the file is considered stale.
* @return {@code true} if the file exists and is older than the retention delay.
*/
private boolean isStale(Path file, Duration retentionDelay) {

Duration age = this.context.getFileAccess().getFileAge(file);
return (age != null) && age.compareTo(retentionDelay) > 0;
}

/**
* Logs a summary of the stale files to be deleted.
*
* @param staleFiles the stale files to report.
* @param retentionDelay the age that the stale files exceed.
*/
private void logStaleFilesToBeDeleted(List<Path> staleFiles, Duration retentionDelay) {

if (staleFiles.isEmpty()) {
LOG.info("No stale files older than {} will be deleted.", retentionDelay);
} else {
for (Path staleFile : staleFiles) {
LOG.info("\t - {} will be deleted", staleFile);
}
LOG.info("Summary: {} stale file(s) older than {} will be deleted.", staleFiles.size(), retentionDelay);
}
}
Comment on lines +504 to +514

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to format the duration human-readably.


/**
* Deletes the given stale files and removes the parent folders that became empty as a result. Folders above the scanned roots are never removed.
*
* @param staleFiles the stale files to delete.
* @param roots the folders that were scanned, which must not be removed themselves.
*/
private void deleteStaleFiles(List<Path> staleFiles, List<Path> roots) {

int failedDeletion = 0;

for (Path staleFile : staleFiles) {
if (Files.exists(staleFile)) {
LOG.debug("Deleting stale file {}", staleFile);
failedDeletion += deleteFolder(staleFile);
}
}

// Remove the folders that became empty after deleting the stale files, walking up from each deleted file but never removing the scanned roots
// themselves.
Set<Path> prunedFolders = new HashSet<>();
for (Path staleFile : staleFiles) {
Path folder = staleFile.getParent();
while (folder != null && !prunedFolders.contains(folder) && !roots.contains(folder)) {
if (!isEmptyFolder(folder)) {
break;
}
LOG.debug("Deleting empty folder {}", folder);
prunedFolders.add(folder);
failedDeletion += deleteFolder(folder);
folder = folder.getParent();
}
}

if (failedDeletion > 0) {
LOG.warn("Stale files have been deleted.\nFailed to delete {} file(s) or folder(s). Please check the log for details.", failedDeletion);
} else {
IdeLogLevel.SUCCESS.log(LOG, "Stale files have been deleted successfully.");
}
}
}
1 change: 1 addition & 0 deletions cli/src/main/resources/nls/Help.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ cmd.claude=Tool commandlet for Claude Code CLI.
cmd.claude.detail=Claude Code CLI is a command-line interface for interacting with the Claude AI assistant. Detailed documentation can be found at https://code.claude.com/docs/en/overview
cmd.cleanup=Commandlet to clean up the IDEasy installation by uninstalling all unused tools.
cmd.cleanup.detail=This will remove any installed tools that are currently not in use by an IDEasy project. Before anything is deleted you are asked for confirmation. Run "ide -b -f cleanup" to skip the confirmation.
Comment on lines 14 to 15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be updated to match the new functionality.

cmd.cleanup.opt.--retention-delay=the retention period of files in the 'updates', '_ide/tmp' and 'Downloads/ide' folders, i.e. files that were not modified within this period are deleted as stale. A time-based ISO-8601 duration (e.g. 'P30D' for 30 days or 'PT2H30M' for 2 hours and 30 minutes). Defaults to 1 year (365 days) if not provided.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cmd.cleanup.opt.--retention-delay=the retention period of files in the 'updates', '_ide/tmp' and 'Downloads/ide' folders, i.e. files that were not modified within this period are deleted as stale. A time-based ISO-8601 duration (e.g. 'P30D' for 30 days or 'PT2H30M' for 2 hours and 30 minutes). Defaults to 1 year (365 days) if not provided.
cmd.cleanup.opt.--retention-delay=The retention period of files in the 'updates', '_ide/tmp' and 'Downloads/ide' folders, i.e. files that were not modified within this period are deleted as stale. A time-based ISO-8601 duration (e.g. 'P30D' for 30 days or 'PT2H30M' for 2 hours and 30 minutes). Defaults to 1 year (365 days) if not provided.

Furthermore the folders you mention here are correct for Windows and Linux, however on macOS there are somewhere else, maybe just remove the path descriptions and describe the folders?

cmd.complete=Internal commandlet for bash auto-completion.
cmd.complete.detail=Run 'ide complete <args>' to activate the non-interactive autocompletion, replace <args> with the arguments you want to autocomplete.\nE.g. type: 'ide complete in' to get 'install' and 'intellij' suggestions.
cmd.copilot=Tool commandlet for GitHub Copilot CLI.
Expand Down
1 change: 1 addition & 0 deletions cli/src/main/resources/nls/Help_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ cmd.claude=Werkzeug Kommando für Claude Code CLI.
cmd.claude.detail=Claude Code CLI ist ein KI-gestützter Programmierassistent, der über die Befehlszeile ausgeführt wird. Detaillierte Dokumentation ist zu finden unter https://code.claude.com/docs/de/overview
cmd.cleanup=Werkzeug zum Aufräumen der IDEasy-Installation durch Deinstallieren aller ungenutzten Werkzeuge.
cmd.cleanup.detail=Dies wird alle installierten Werkzeuge entfernen, die derzeit von keinem IDEasy-Projekt verwendet werden. Bevor etwas gelöscht wird, wirst du um Bestätigung gebeten. Führe "ide -b -f cleanup" aus, um die Bestätigung zu überspringen.
Comment on lines 14 to 15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be updated to match the new functionality.

cmd.cleanup.opt.--retention-delay=die Aufbewahrungsdauer von Dateien in den Ordnern 'updates', '_ide/tmp' und 'Downloads/ide', d.h. Dateien, die innerhalb dieses Zeitraums nicht modifiziert wurden, werden als veraltet gelöscht. Eine zeitbasierte ISO-8601-Dauer (z. B. 'P30D' für 30 Tage oder 'PT2H30M' für 2 Stunden und 30 Minuten). Standardmäßig 1 Jahr (365 Tage), wenn nicht angegeben.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cmd.cleanup.opt.--retention-delay=die Aufbewahrungsdauer von Dateien in den Ordnern 'updates', '_ide/tmp' und 'Downloads/ide', d.h. Dateien, die innerhalb dieses Zeitraums nicht modifiziert wurden, werden als veraltet gelöscht. Eine zeitbasierte ISO-8601-Dauer (z. B. 'P30D' für 30 Tage oder 'PT2H30M' für 2 Stunden und 30 Minuten). Standardmäßig 1 Jahr (365 Tage), wenn nicht angegeben.
cmd.cleanup.opt.--retention-delay=Die Altersgrenze von Dateien in den Ordnern 'updates', '_ide/tmp' und 'Downloads/ide', d.h. Dateien, die innerhalb dieses Zeitraums nicht modifiziert wurden, werden als veraltet gelöscht. Eine zeitbasierte ISO-8601-Dauer (z. B. 'P30D' für 30 Tage oder 'PT2H30M' für 2 Stunden und 30 Minuten). Standardmäßig 1 Jahr (365 Tage), wenn nicht angegeben.

Furthermore the folders you mention here are correct for Windows and Linux, however on macOS there are somewhere else, maybe just remove the path descriptions and describe the folders?

cmd.complete=Internes Werkzeug für bash Autovervollständigung.
cmd.complete.detail=Geben Sie 'ide complete <ausdruck>' in die Konsole ein um die einfache Autovervollständigung zu aktivieren, ersetzen Sie <ausdruck> mit dem Ausdruck, der automatisch vervollständigt werden soll.\nZ.B. geben Sie einfach 'ide complete in' in die Konsole ein um 'install' und 'intellij' als Vorschläge zu erhalten.
cmd.copilot=Werkzeug Kommando für GitHub Copilot CLI.
Expand Down
Loading