From a5ce507a1ad527bc6ad05e0e24b422507a6f846f Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Fri, 15 May 2026 16:05:24 +0200 Subject: [PATCH 01/27] First push --- .../ide/commandlet/CleanupCommandlet.java | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java new file mode 100644 index 0000000000..2fccdb4644 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -0,0 +1,63 @@ +package com.devonfw.tools.ide.commandlet; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import com.devonfw.tools.ide.context.IdeContext; + +public class CleanupCommandlet extends Commandlet{ + + public CleanupCommandlet(IdeContext context) { + + super(context); + addKeyword(getName()); + } + + @Override + public String getName() { + + return "cleanup"; + } + + @Override + protected void doRun() { + + // Scan for IDEasy projects + List ideasyProjects = this.context.getFileAccess().listChildren(this.context.getIdeRoot(), Files::isDirectory); + + // Iterate through IDEasy projects and scan software in software folder. Save found software to list + for (Path ideasyProject : ideasyProjects) { + scan_software_folder(ideasyProject.resolve("software")); + } + + + // Iterate over software in $IDE_ROOT/_ide/software folder and save unused software to a list + + // Remove unused software + + + } + + private void scan_software_folder(Path software_folder) { + + // Check if directory contains "ide.software.version" file. If so, read version and add to list of used software + if (Files.exists(software_folder.resolve("ide.software.version"))) { + // read version and add to list of used software + return; + } + + // If not, get all subfolders and recursively iterate over them + try { + List subfolders = this.context.getFileAccess().listChildren(software_folder, Files::isDirectory); + for (Path subfolder : subfolders) { + scan_software_folder(subfolder); + } + } catch (Exception e) { + // log error and return + return; + } + + } + +} From f4ed98268aaa75c3f2281994ab6f80608da4cb90 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Mon, 18 May 2026 09:29:29 +0200 Subject: [PATCH 02/27] Allow discvery of software and unused software --- .../ide/commandlet/CleanupCommandlet.java | 61 ++++++++++++++++--- .../ide/commandlet/CommandletManagerImpl.java | 1 + 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index 2fccdb4644..60074c8aba 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -4,10 +4,33 @@ import java.nio.file.Path; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.devonfw.tools.ide.context.IdeContext; public class CleanupCommandlet extends Commandlet{ + private static final Logger LOG = LoggerFactory.getLogger(CleanupCommandlet.class); + + private static class IdeTool { + String tool_name; + String tool_edition; + String tool_version; + private final Path path; + private boolean used; + + IdeTool(String tool_name, String tool_edition, String tool_version, Path path) { + this.tool_name = tool_name; + this.tool_edition = tool_edition; + this.tool_version = tool_version; + this.path = path; + this.used = false; + } + } + + private final List installedIdeTools = new java.util.ArrayList<>(); + public CleanupCommandlet(IdeContext context) { super(context); @@ -23,27 +46,51 @@ public String getName() { @Override protected void doRun() { + LOG.debug("Start cleanup commandlet"); + // Iterate over software in $IDE_ROOT/_ide/software folder and save installed software to a list + scanSoftwareFolder(this.context.getIdeRoot().resolve("_ide").resolve("software"), true); + // Scan for IDEasy projects List ideasyProjects = this.context.getFileAccess().listChildren(this.context.getIdeRoot(), Files::isDirectory); // Iterate through IDEasy projects and scan software in software folder. Save found software to list for (Path ideasyProject : ideasyProjects) { - scan_software_folder(ideasyProject.resolve("software")); + if (ideasyProject.getFileName().toString().equals("_ide")) { + continue; + } + scanSoftwareFolder(ideasyProject.resolve("software"), false); } - // Iterate over software in $IDE_ROOT/_ide/software folder and save unused software to a list // Remove unused software } - private void scan_software_folder(Path software_folder) { + private void scanSoftwareFolder(Path software_folder, Boolean discover) { - // Check if directory contains "ide.software.version" file. If so, read version and add to list of used software - if (Files.exists(software_folder.resolve("ide.software.version"))) { - // read version and add to list of used software + software_folder = this.context.getFileAccess().toRealPath(software_folder); + LOG.debug("Scanning software folder: " +software_folder); // DEBUG + // Check if directory contains ".ide.software.version" file. If so, read version and add to list of used software + if (Files.exists(software_folder.resolve(".ide.software.version"))) { + LOG.debug("Found software folder: " + software_folder); // DEBUG + if (discover) { + String tool_name = software_folder.getParent().getParent().getFileName().toString(); + String tool_edition = software_folder.getParent().getFileName().toString(); + String tool_version = software_folder.getFileName().toString(); + LOG.debug("Discovered software at path: " + software_folder); // DEBUG + this.installedIdeTools.add(new IdeTool(tool_name, tool_edition, tool_version, software_folder)); + } else { + // Check if software with name and version exists in IdeTool list. If so, mark as used + for (IdeTool tool : this.installedIdeTools) { + if (tool.path.toString().equals(software_folder.toString())) { + tool.used = true; + LOG.debug("Marked software as used: " + software_folder); // DEBUG + break; + } + } + } return; } @@ -51,7 +98,7 @@ private void scan_software_folder(Path software_folder) { try { List subfolders = this.context.getFileAccess().listChildren(software_folder, Files::isDirectory); for (Path subfolder : subfolders) { - scan_software_folder(subfolder); + scanSoftwareFolder(subfolder, discover); } } catch (Exception e) { // log error and return diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index 104f33954d..ef4b4f8b69 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -108,6 +108,7 @@ public CommandletManagerImpl(IdeContext context) { add(new RepositoryCommandlet(context)); add(new UninstallCommandlet(context)); add(new UpdateCommandlet(context)); + add(new CleanupCommandlet(context)); add(new UpgradeSettingsCommandlet(context)); add(new CreateCommandlet(context)); add(new BuildCommandlet(context)); From c3cfee4614f6e0b2693320e8a1fac8eace6fea27 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Tue, 19 May 2026 14:23:01 +0200 Subject: [PATCH 03/27] Further Implementation --- .../ide/commandlet/CleanupCommandlet.java | 262 +++++++++++++++--- 1 file changed, 223 insertions(+), 39 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index 60074c8aba..d5bb63e43f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -7,25 +7,53 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.devonfw.tools.ide.cli.CliAbortException; import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.io.FileAccess; +import com.devonfw.tools.ide.log.IdeLogLevel; +import com.devonfw.tools.ide.property.FlagProperty; public class CleanupCommandlet extends Commandlet{ + public final FlagProperty forceDelete; + private static final Logger LOG = LoggerFactory.getLogger(CleanupCommandlet.class); + private static class IdeToolEditionVersion { + String version_name; + private final Path path; + private List used_by = new java.util.ArrayList<>(); + private boolean delete = false; + + IdeToolEditionVersion(String version_name, Path path) { + this.version_name = version_name; + this.path = path; + } + } + + private static class IdeToolEdition { + String edition_name; + private final Path path; + private List used_by = new java.util.ArrayList<>(); + private boolean delete = false; + private List versions = new java.util.ArrayList<>(); + + IdeToolEdition(String edition_name, Path path) { + this.edition_name = edition_name; + this.path = path; + } + } + private static class IdeTool { String tool_name; - String tool_edition; - String tool_version; private final Path path; - private boolean used; + private List used_by = new java.util.ArrayList<>(); + private boolean delete = false; + private List editions = new java.util.ArrayList<>(); - IdeTool(String tool_name, String tool_edition, String tool_version, Path path) { + IdeTool(String tool_name, Path path) { this.tool_name = tool_name; - this.tool_edition = tool_edition; - this.tool_version = tool_version; this.path = path; - this.used = false; } } @@ -35,7 +63,9 @@ public CleanupCommandlet(IdeContext context) { super(context); addKeyword(getName()); + this.forceDelete = add(new FlagProperty("--fd")); } + @Override public String getName() { @@ -48,7 +78,7 @@ protected void doRun() { LOG.debug("Start cleanup commandlet"); // Iterate over software in $IDE_ROOT/_ide/software folder and save installed software to a list - scanSoftwareFolder(this.context.getIdeRoot().resolve("_ide").resolve("software"), true); + discoverInstalledSoftware(this.context.getIdeRoot().resolve("_ide/software/default")); // Scan for IDEasy projects List ideasyProjects = this.context.getFileAccess().listChildren(this.context.getIdeRoot(), Files::isDirectory); @@ -58,53 +88,207 @@ protected void doRun() { if (ideasyProject.getFileName().toString().equals("_ide")) { continue; } - scanSoftwareFolder(ideasyProject.resolve("software"), false); + discoverUsedSoftware(ideasyProject.resolve("software"), ideasyProject.getFileName().toString()); } + String hello = "hello"; + // Remove unused software + markUnusedSoftwareForDeletion(); + logSoftwareToBeDeleted(); + //deleteUnusedSoftware(); - // Remove unused software + } + private void discoverInstalledSoftware(Path software_folder) { + List subfolders = this.context.getFileAccess().listChildren(software_folder, Files::isDirectory); + for (Path subfolder : subfolders) { + IdeTool tool = new IdeTool(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); + this.installedIdeTools.add(tool); + discoverInstalledEditions(subfolder, tool); + } + } + + private void discoverInstalledEditions(Path edition_folder, IdeTool tool) { + List subfolders = this.context.getFileAccess().listChildren(edition_folder, Files::isDirectory); + for (Path subfolder : subfolders) { + IdeToolEdition edition = new IdeToolEdition(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); + tool.editions.add(edition); + discoverInstalledVersions(subfolder, edition); + } + } + private void discoverInstalledVersions(Path version_folder, IdeToolEdition edition) { + List subfolders = this.context.getFileAccess().listChildren(version_folder, Files::isDirectory); + for (Path subfolder : subfolders) { + IdeToolEditionVersion version = new IdeToolEditionVersion(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); + edition.versions.add(version); + } } - private void scanSoftwareFolder(Path software_folder, Boolean discover) { - - software_folder = this.context.getFileAccess().toRealPath(software_folder); - LOG.debug("Scanning software folder: " +software_folder); // DEBUG + private void discoverUsedSoftware(Path current_folder, String project_name) { + current_folder = this.context.getFileAccess().toRealPath(current_folder); // Check if directory contains ".ide.software.version" file. If so, read version and add to list of used software - if (Files.exists(software_folder.resolve(".ide.software.version"))) { - LOG.debug("Found software folder: " + software_folder); // DEBUG - if (discover) { - String tool_name = software_folder.getParent().getParent().getFileName().toString(); - String tool_edition = software_folder.getParent().getFileName().toString(); - String tool_version = software_folder.getFileName().toString(); - LOG.debug("Discovered software at path: " + software_folder); // DEBUG - this.installedIdeTools.add(new IdeTool(tool_name, tool_edition, tool_version, software_folder)); - } else { - // Check if software with name and version exists in IdeTool list. If so, mark as used - for (IdeTool tool : this.installedIdeTools) { - if (tool.path.toString().equals(software_folder.toString())) { - tool.used = true; - LOG.debug("Marked software as used: " + software_folder); // DEBUG - break; + if (Files.exists(current_folder.resolve(".ide.software.version"))) { + if (!current_folder.startsWith(this.context.getIdeRoot().resolve("_ide/software/default"))) { + // We found a software that is directly installed in an IDEasy project but not in the global software folder. We leave these alone + return; + } + String tool_name = current_folder.getParent().getParent().getFileName().toString(); + String tool_edition = current_folder.getParent().getFileName().toString(); + String tool_version = current_folder.getFileName().toString(); + // Check if software exists in IdeTool list. If so, mark as used + for (IdeTool tool : this.installedIdeTools) { + if (tool.tool_name.equals(tool_name)) { + tool.used_by.add(project_name); + for (IdeToolEdition edition : tool.editions) { + if (edition.edition_name.equals(tool_edition)) { + edition.used_by.add(project_name); + for (IdeToolEditionVersion version : edition.versions) { + if (version.version_name.equals(tool_version)) { + version.used_by.add(project_name); + break; + } + } + break; + } + } + break; + } + } + } else { + // If not, get all subfolders and recursively iterate over them + try { + List subfolders = this.context.getFileAccess().listChildren(current_folder, Files::isDirectory); + for (Path subfolder : subfolders) { + discoverUsedSoftware(subfolder, project_name); + } + } catch (Exception e) { + // log error and return + return; + } + } + } + + private void markUnusedSoftwareForDeletion() { + for (IdeTool tool : this.installedIdeTools) { + for (IdeToolEdition edition : tool.editions) { + for (IdeToolEditionVersion version : edition.versions) { + if (version.used_by.isEmpty()) { + version.delete = true; } } + if (edition.used_by.isEmpty()) { + edition.delete = true; + } + } + if (tool.used_by.isEmpty()) { + tool.delete = true; } - return; } + } - // If not, get all subfolders and recursively iterate over them - try { - List subfolders = this.context.getFileAccess().listChildren(software_folder, Files::isDirectory); - for (Path subfolder : subfolders) { - scanSoftwareFolder(subfolder, discover); + private void logSoftwareToBeDeleted() { + String LogOutput = ""; + int totalToolsDeleted = 0; + int totalEditionsDeleted = 0; + int totalVersionsDeleted = 0; + for (IdeTool tool : this.installedIdeTools) { + String LogOutputEdition = ""; + int editionsDeleted = 0; + for (IdeToolEdition edition : tool.editions) { + String LogOutputVersion = ""; + int versionsDeleted = 0; + for (IdeToolEditionVersion version : edition.versions) { + if (version.delete) { + LogOutputVersion += "\t\t - " + version.version_name + "\n"; + versionsDeleted++; + totalVersionsDeleted++; + } + } + if (!LogOutputVersion.isBlank()) { + if (versionsDeleted < edition.versions.size()) { + LogOutputVersion += "\t\t + " + (edition.versions.size() - versionsDeleted) + " more version(s) of this edition will not be deleted\n"; + LogOutputEdition += "\t - " + edition.edition_name + "\n" + LogOutputVersion; + } + editionsDeleted++; + totalEditionsDeleted++; + } + } + if (!LogOutputEdition.isBlank()) { + if (editionsDeleted < tool.editions.size()) { + LogOutputEdition += "\t + " + (tool.editions.size() - editionsDeleted) + " more edition(s) of this tool will not be deleted\n"; + } + LogOutput += " - " + tool.tool_name + "\n" + LogOutputEdition; + totalToolsDeleted++; } - } catch (Exception e) { - // log error and return - return; } + if (LogOutput.isBlank()) { + LOG.info("No installed tools will be deleted. All installed software is used by at least one project."); + } else { + LOG.info("The following installed tools will be deleted: \n" + LogOutput); + LOG.info("Summary: {} installed tool versions across {} editions of {} tools will be deleted.", totalVersionsDeleted, totalEditionsDeleted, totalToolsDeleted); + + if (!this.forceDelete.isTrue()) { + try { + this.context.askToContinue("Do you want to continue?"); + + } catch (CliAbortException e) { + LOG.info("Installed Tools will not be deleted."); + return; + } + } + deleteUnusedSoftware(); + } } -} + private void deleteUnusedSoftware() { + int failed_deletion = 0; + FileAccess fileAccess = this.context.getFileAccess(); + // Delete the tool + for (IdeTool tool : this.installedIdeTools) { + if (tool.delete) { + LOG.debug("Deleting tool {} and all its editions and versions in {}", tool.tool_name, tool.path); + try { + //fileAccess.delete(tool.path); + } catch (Exception e) { + LOG.error("Failed to delete {}: {}", tool.path, e.getMessage()); + failed_deletion++; + } + continue; + } + // Delete editions of the tool + for (IdeToolEdition edition : tool.editions) { + if (edition.delete) { + LOG.debug("Deleting edition {} of tool {} and all its versions in {}", edition.edition_name, tool.tool_name, edition.path); + try { + //fileAccess.delete(edition.path); + } catch (Exception e) { + LOG.error("Failed to delete {}: {}", edition.path, e.getMessage()); + failed_deletion++; + } + continue; + } + // Delete versions of the edition + for (IdeToolEditionVersion version : edition.versions) { + if (version.delete) { + LOG.debug("Deleting version {} of edition {} of tool {} in {}", version.version_name, edition.edition_name, tool.tool_name, version.path); + try { + //this.context.getFileAccess().delete(version.path); + } catch (Exception e) { + LOG.error("Failed to delete {}: {}", version.path, e.getMessage()); + failed_deletion++; + } + } + } + } + } + + if (failed_deletion > 0) { + LOG.warn("Unused tools have been deleted.\nFailed to delete {} files. Please check the log for details.", failed_deletion); + } else { + IdeLogLevel.SUCCESS.log(LOG, "Unused tools have been deleted successfully."); + } + } +} \ No newline at end of file From eea15aebb9f4ce87bc6176ccc63fdce66061e19c Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Tue, 19 May 2026 17:05:33 +0200 Subject: [PATCH 04/27] Update Help descriptions --- cli/src/main/resources/nls/Help.properties | 3 +++ cli/src/main/resources/nls/Help_de.properties | 3 +++ 2 files changed, 6 insertions(+) diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 34a6b8b72b..8150d4070a 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -9,6 +9,8 @@ cmd.build.detail=The `build` commandlet is an abstraction of build systems like cmd.build.val.args=Build arguments to be used when running a build job. cmd.cdk=Tool commandlet for AWS CDK. cmd.cdk.detail=The AWS Cloud Development Kit (AWS CDK) is an open-source software development framework for defining cloud infrastructure in code and provisioning it through AWS CloudFormation. Detailed documentation can be found at https://docs.aws.amazon.com/cdk/v2/guide/home.html +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. cmd.complete=Internal commandlet for bash auto-completion. cmd.complete.detail=Run 'ide complete ' to activate the non-interactive autocompletion, replace 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. @@ -153,6 +155,7 @@ icd-hint=Hint: Use 'icd' command to easily navigate between your IDE home, proje opt.--batch=enable batch mode (non-interactive). opt.--code=clone given code repository containing a settings folder into workspaces so that settings can be committed alongside code changes. opt.--debug=enable debug logging. +opt.--fd=Delete tools without confirmation opt.--force=enable force mode. opt.--force-plugins=force plugin (re)installation. opt.--force-pull=force pull of settings even for code repository. diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index ab3062fd2a..47b19cea23 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -9,6 +9,8 @@ cmd.build.detail=Der `build`-Befehl ist eine Abstraktion von Build-Systemen wie cmd.build.val.args=Argumente die bei dem build Prozess genutzt werden sollen. cmd.cdk=Werkzeug Kommando für AWS CDK. cmd.cdk.detail=Das AWS Cloud Development Kit (AWS CDK) ist ein Open-Source-Softwareentwicklungsframework zur Modellierung und Bereitstellung von Cloud-Anwendungen mithilfe von bekannten Programmiersprachen. Detaillierte Dokumentation ist zu finden unter https://docs.aws.amazon.com/cdk/v2/guide/home.html +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. cmd.complete=Internes Werkzeug für bash Autovervollständigung. cmd.complete.detail=Geben Sie 'ide complete ' in die Konsole ein um die einfache Autovervollständigung zu aktivieren, ersetzen Sie 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. @@ -153,6 +155,7 @@ icd-hint=Hinweis: Verwenden Sie den Befehl 'icd' um einfach zwischen Ihrem IDE-H opt.--batch=Aktiviert den Batch-Modus (nicht-interaktive Stapelverarbeitung). opt.--code=Git-Repository sowohl als Code- als auch als Settings-Repository verwenden. opt.--debug=Aktiviert Debug-Ausgaben (Fehleranalyse). +opt.--fd=Ungenutzte Werkzeuge ohne Bestätigung löschen. opt.--force=Aktiviert den Force-Modus (Erzwingen). opt.--force-plugins=Erzwingt die (Re)Installation von Plugins. opt.--force-pull=Erzwingt einen Pull der settings auch im Fall eines Code-Repositories. From a4d933a70238bc73b7aeb834915ece70d5d163af Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Wed, 20 May 2026 08:26:44 +0200 Subject: [PATCH 05/27] Finish Base Implementation --- .../ide/commandlet/CleanupCommandlet.java | 82 +++++++++---------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index d5bb63e43f..162d00ec55 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -12,6 +12,7 @@ import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.property.FlagProperty; +import com.devonfw.tools.ide.step.Step; public class CleanupCommandlet extends Commandlet{ @@ -77,6 +78,14 @@ public String getName() { protected void doRun() { LOG.debug("Start cleanup commandlet"); + + Step step = context.newStep("Identify and remove unused software"); + step.run(() -> {discoverAndDeleteUnusedSoftware();}); + + LOG.debug("Finished cleanup commandlet"); + } + + private void discoverAndDeleteUnusedSoftware() { // Iterate over software in $IDE_ROOT/_ide/software folder and save installed software to a list discoverInstalledSoftware(this.context.getIdeRoot().resolve("_ide/software/default")); @@ -89,15 +98,12 @@ protected void doRun() { continue; } discoverUsedSoftware(ideasyProject.resolve("software"), ideasyProject.getFileName().toString()); + discoverUsedSoftware(ideasyProject.resolve("software/extra"), ideasyProject.getFileName().toString()); } - String hello = "hello"; // Remove unused software markUnusedSoftwareForDeletion(); logSoftwareToBeDeleted(); - //deleteUnusedSoftware(); - - } private void discoverInstalledSoftware(Path software_folder) { @@ -126,46 +132,38 @@ private void discoverInstalledVersions(Path version_folder, IdeToolEdition editi } } - private void discoverUsedSoftware(Path current_folder, String project_name) { - current_folder = this.context.getFileAccess().toRealPath(current_folder); - // Check if directory contains ".ide.software.version" file. If so, read version and add to list of used software - if (Files.exists(current_folder.resolve(".ide.software.version"))) { - if (!current_folder.startsWith(this.context.getIdeRoot().resolve("_ide/software/default"))) { - // We found a software that is directly installed in an IDEasy project but not in the global software folder. We leave these alone - return; - } - String tool_name = current_folder.getParent().getParent().getFileName().toString(); - String tool_edition = current_folder.getParent().getFileName().toString(); - String tool_version = current_folder.getFileName().toString(); - // Check if software exists in IdeTool list. If so, mark as used - for (IdeTool tool : this.installedIdeTools) { - if (tool.tool_name.equals(tool_name)) { - tool.used_by.add(project_name); - for (IdeToolEdition edition : tool.editions) { - if (edition.edition_name.equals(tool_edition)) { - edition.used_by.add(project_name); - for (IdeToolEditionVersion version : edition.versions) { - if (version.version_name.equals(tool_version)) { - version.used_by.add(project_name); - break; + private void discoverUsedSoftware(Path software_folder, String project_name) { + List subfolders = this.context.getFileAccess().listChildren(software_folder, Files::isDirectory); + for (Path current_folder : subfolders) { + current_folder = this.context.getFileAccess().toRealPath(current_folder); + // Check if directory contains ".ide.software.version" file. If so, read version and add to list of used software + if (Files.exists(current_folder.resolve(".ide.software.version"))) { + if (!current_folder.startsWith(this.context.getIdeRoot().resolve("_ide/software/default"))) { + // We found a software that is directly installed in an IDEasy project but not in the global software folder. We leave these alone + return; + } + String tool_name = current_folder.getParent().getParent().getFileName().toString(); + String tool_edition = current_folder.getParent().getFileName().toString(); + String tool_version = current_folder.getFileName().toString(); + // Check if software exists in IdeTool list. If so, mark as used + for (IdeTool tool : this.installedIdeTools) { + if (tool.tool_name.equals(tool_name)) { + tool.used_by.add(project_name); + for (IdeToolEdition edition : tool.editions) { + if (edition.edition_name.equals(tool_edition)) { + edition.used_by.add(project_name); + for (IdeToolEditionVersion version : edition.versions) { + if (version.version_name.equals(tool_version)) { + version.used_by.add(project_name); + break; + } } + break; } - break; } + break; } - break; - } - } - } else { - // If not, get all subfolders and recursively iterate over them - try { - List subfolders = this.context.getFileAccess().listChildren(current_folder, Files::isDirectory); - for (Path subfolder : subfolders) { - discoverUsedSoftware(subfolder, project_name); } - } catch (Exception e) { - // log error and return - return; } } } @@ -251,7 +249,7 @@ private void deleteUnusedSoftware() { if (tool.delete) { LOG.debug("Deleting tool {} and all its editions and versions in {}", tool.tool_name, tool.path); try { - //fileAccess.delete(tool.path); + fileAccess.delete(tool.path); } catch (Exception e) { LOG.error("Failed to delete {}: {}", tool.path, e.getMessage()); failed_deletion++; @@ -263,7 +261,7 @@ private void deleteUnusedSoftware() { if (edition.delete) { LOG.debug("Deleting edition {} of tool {} and all its versions in {}", edition.edition_name, tool.tool_name, edition.path); try { - //fileAccess.delete(edition.path); + fileAccess.delete(edition.path); } catch (Exception e) { LOG.error("Failed to delete {}: {}", edition.path, e.getMessage()); failed_deletion++; @@ -275,7 +273,7 @@ private void deleteUnusedSoftware() { if (version.delete) { LOG.debug("Deleting version {} of edition {} of tool {} in {}", version.version_name, edition.edition_name, tool.tool_name, version.path); try { - //this.context.getFileAccess().delete(version.path); + fileAccess.delete(version.path); } catch (Exception e) { LOG.error("Failed to delete {}: {}", version.path, e.getMessage()); failed_deletion++; From 6af255894b294220b4434debe55ddee66708aa54 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Wed, 20 May 2026 08:42:21 +0200 Subject: [PATCH 06/27] Added changelog entry --- CHANGELOG.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index bbfe750876..17f9fa9166 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -31,6 +31,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/800[#800]: Fix infinite recursion in Sonar start/stop on macOS * https://github.com/devonfw/IDEasy/issues/1716[#1716]: Add commandlet for Claude Code CLI * https://github.com/devonfw/IDEasy/issues/1844[#1844]: Fix vscode installation hanging indefinitely in WSL Linux environments +* https://github.com/devonfw/IDEasy/issues/1953[#1953]: Implement base functionality of cleanup commandlet The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/44?closed=1[milestone 2026.05.001]. From bf631250c0fb3782b9c21b91d12a0991c7fc2372 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Wed, 20 May 2026 08:45:18 +0200 Subject: [PATCH 07/27] Changed variable names to follow Coding conventions --- .../ide/commandlet/CleanupCommandlet.java | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index 162d00ec55..c6ea114be5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -21,39 +21,39 @@ public class CleanupCommandlet extends Commandlet{ private static final Logger LOG = LoggerFactory.getLogger(CleanupCommandlet.class); private static class IdeToolEditionVersion { - String version_name; + String versionName; private final Path path; - private List used_by = new java.util.ArrayList<>(); + private List usedBy = new java.util.ArrayList<>(); private boolean delete = false; - IdeToolEditionVersion(String version_name, Path path) { - this.version_name = version_name; + IdeToolEditionVersion(String versionName, Path path) { + this.versionName = versionName; this.path = path; } } private static class IdeToolEdition { - String edition_name; + String editionName; private final Path path; - private List used_by = new java.util.ArrayList<>(); + private List usedBy = new java.util.ArrayList<>(); private boolean delete = false; private List versions = new java.util.ArrayList<>(); - IdeToolEdition(String edition_name, Path path) { - this.edition_name = edition_name; + IdeToolEdition(String editionName, Path path) { + this.editionName = editionName; this.path = path; } } private static class IdeTool { - String tool_name; + String toolName; private final Path path; - private List used_by = new java.util.ArrayList<>(); + private List usedBy = new java.util.ArrayList<>(); private boolean delete = false; private List editions = new java.util.ArrayList<>(); - IdeTool(String tool_name, Path path) { - this.tool_name = tool_name; + IdeTool(String toolName, Path path) { + this.toolName = toolName; this.path = path; } } @@ -147,14 +147,14 @@ private void discoverUsedSoftware(Path software_folder, String project_name) { String tool_version = current_folder.getFileName().toString(); // Check if software exists in IdeTool list. If so, mark as used for (IdeTool tool : this.installedIdeTools) { - if (tool.tool_name.equals(tool_name)) { - tool.used_by.add(project_name); + if (tool.toolName.equals(tool_name)) { + tool.usedBy.add(project_name); for (IdeToolEdition edition : tool.editions) { - if (edition.edition_name.equals(tool_edition)) { - edition.used_by.add(project_name); + if (edition.editionName.equals(tool_edition)) { + edition.usedBy.add(project_name); for (IdeToolEditionVersion version : edition.versions) { - if (version.version_name.equals(tool_version)) { - version.used_by.add(project_name); + if (version.versionName.equals(tool_version)) { + version.usedBy.add(project_name); break; } } @@ -172,15 +172,15 @@ private void markUnusedSoftwareForDeletion() { for (IdeTool tool : this.installedIdeTools) { for (IdeToolEdition edition : tool.editions) { for (IdeToolEditionVersion version : edition.versions) { - if (version.used_by.isEmpty()) { + if (version.usedBy.isEmpty()) { version.delete = true; } } - if (edition.used_by.isEmpty()) { + if (edition.usedBy.isEmpty()) { edition.delete = true; } } - if (tool.used_by.isEmpty()) { + if (tool.usedBy.isEmpty()) { tool.delete = true; } } @@ -199,7 +199,7 @@ private void logSoftwareToBeDeleted() { int versionsDeleted = 0; for (IdeToolEditionVersion version : edition.versions) { if (version.delete) { - LogOutputVersion += "\t\t - " + version.version_name + "\n"; + LogOutputVersion += "\t\t - " + version.versionName + "\n"; versionsDeleted++; totalVersionsDeleted++; } @@ -207,7 +207,7 @@ private void logSoftwareToBeDeleted() { if (!LogOutputVersion.isBlank()) { if (versionsDeleted < edition.versions.size()) { LogOutputVersion += "\t\t + " + (edition.versions.size() - versionsDeleted) + " more version(s) of this edition will not be deleted\n"; - LogOutputEdition += "\t - " + edition.edition_name + "\n" + LogOutputVersion; + LogOutputEdition += "\t - " + edition.editionName + "\n" + LogOutputVersion; } editionsDeleted++; totalEditionsDeleted++; @@ -217,7 +217,7 @@ private void logSoftwareToBeDeleted() { if (editionsDeleted < tool.editions.size()) { LogOutputEdition += "\t + " + (tool.editions.size() - editionsDeleted) + " more edition(s) of this tool will not be deleted\n"; } - LogOutput += " - " + tool.tool_name + "\n" + LogOutputEdition; + LogOutput += " - " + tool.toolName + "\n" + LogOutputEdition; totalToolsDeleted++; } } @@ -247,7 +247,7 @@ private void deleteUnusedSoftware() { // Delete the tool for (IdeTool tool : this.installedIdeTools) { if (tool.delete) { - LOG.debug("Deleting tool {} and all its editions and versions in {}", tool.tool_name, tool.path); + LOG.debug("Deleting tool {} and all its editions and versions in {}", tool.toolName, tool.path); try { fileAccess.delete(tool.path); } catch (Exception e) { @@ -259,7 +259,7 @@ private void deleteUnusedSoftware() { // Delete editions of the tool for (IdeToolEdition edition : tool.editions) { if (edition.delete) { - LOG.debug("Deleting edition {} of tool {} and all its versions in {}", edition.edition_name, tool.tool_name, edition.path); + LOG.debug("Deleting edition {} of tool {} and all its versions in {}", edition.editionName, tool.toolName, edition.path); try { fileAccess.delete(edition.path); } catch (Exception e) { @@ -271,7 +271,7 @@ private void deleteUnusedSoftware() { // Delete versions of the edition for (IdeToolEditionVersion version : edition.versions) { if (version.delete) { - LOG.debug("Deleting version {} of edition {} of tool {} in {}", version.version_name, edition.edition_name, tool.tool_name, version.path); + LOG.debug("Deleting version {} of edition {} of tool {} in {}", version.versionName, edition.editionName, tool.toolName, version.path); try { fileAccess.delete(version.path); } catch (Exception e) { From a094a7d35a0c1a9646466163df0a33f6dcbacb1a Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Wed, 20 May 2026 09:10:17 +0200 Subject: [PATCH 08/27] Add comments, method and class descriptions --- .../ide/commandlet/CleanupCommandlet.java | 83 ++++++++++++++++--- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index c6ea114be5..832155a2bc 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -14,16 +14,24 @@ import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.step.Step; +/** +* Commandlet which scans your IDE installation for unused software (tools not currently used by any project) and removes them. +*/ public class CleanupCommandlet extends Commandlet{ public final FlagProperty forceDelete; private static final Logger LOG = LoggerFactory.getLogger(CleanupCommandlet.class); + /** + * Class which represents individual IDE tools. Contains multiple parameter such as the name of the tool, the path where it is installed, + * a list of projects which use this tool and a boolean whether the tool is marked for deletion or not. + * Furthermore, it contains a list of editions of the edition (e.g. for intellij, we could have the editions "intellij" or "ultimate"). + */ private static class IdeToolEditionVersion { String versionName; private final Path path; - private List usedBy = new java.util.ArrayList<>(); + private final List usedBy = new java.util.ArrayList<>(); private boolean delete = false; IdeToolEditionVersion(String versionName, Path path) { @@ -32,12 +40,17 @@ private static class IdeToolEditionVersion { } } + /** + * Class which represents individual editions of an IDE tool. Contains multiple parameter such as the name of the edition, the path where it is installed, + * a list of projects which use this edition and a boolean whether the edition is marked for deletion or not. + * Furthermore, it contains a list of versions of the edition (e.g. for intellij ultimate edition, we could have version 2022.3 and 2023.1 installed). + */ private static class IdeToolEdition { String editionName; private final Path path; - private List usedBy = new java.util.ArrayList<>(); + private final List usedBy = new java.util.ArrayList<>(); private boolean delete = false; - private List versions = new java.util.ArrayList<>(); + private final List versions = new java.util.ArrayList<>(); IdeToolEdition(String editionName, Path path) { this.editionName = editionName; @@ -45,12 +58,16 @@ private static class IdeToolEdition { } } + /** + * Class which represents individual versions of an edition of an IDE tool. Contains multiple parameter such as the name of the version, the path where it is installed, + * a list of projects which use this version and a boolean whether the version is marked for deletion or not. + */ private static class IdeTool { String toolName; private final Path path; - private List usedBy = new java.util.ArrayList<>(); + private final List usedBy = new java.util.ArrayList<>(); private boolean delete = false; - private List editions = new java.util.ArrayList<>(); + private final List editions = new java.util.ArrayList<>(); IdeTool(String toolName, Path path) { this.toolName = toolName; @@ -58,13 +75,14 @@ private static class IdeTool { } } + // List of installed IDE tools in the global software folder at $IDE_ROOT/_ide/software/default. This list is populated at the beginning of the cleanup process and then used to identify unused software and delete it. private final List installedIdeTools = new java.util.ArrayList<>(); public CleanupCommandlet(IdeContext context) { super(context); addKeyword(getName()); - this.forceDelete = add(new FlagProperty("--fd")); + this.forceDelete = add(new FlagProperty("--fd")); // Force-Delete flag. Skips confirmation prompts if provided } @@ -79,12 +97,16 @@ protected void doRun() { LOG.debug("Start cleanup commandlet"); + // Identify and remove unused tools. Step step = context.newStep("Identify and remove unused software"); step.run(() -> {discoverAndDeleteUnusedSoftware();}); LOG.debug("Finished cleanup commandlet"); } + /** + * This method specified the primary flow for the discovery of installed and unused software, and its deletion. + */ private void discoverAndDeleteUnusedSoftware() { // Iterate over software in $IDE_ROOT/_ide/software folder and save installed software to a list discoverInstalledSoftware(this.context.getIdeRoot().resolve("_ide/software/default")); @@ -101,11 +123,17 @@ private void discoverAndDeleteUnusedSoftware() { discoverUsedSoftware(ideasyProject.resolve("software/extra"), ideasyProject.getFileName().toString()); } - // Remove unused software + // Mark unused software for deletion markUnusedSoftwareForDeletion(); + // Log summary report and proceed with deletion if user confirms logSoftwareToBeDeleted(); } + /** + * This method discovers all installed tools at $IDE_ROOT/_ide/software/default and saves them to the list of installed tools. + * Installed editions are then recusively discovered + * @param software_folder The folder where the software is saved in ($IDE_ROOT/_ide/software/default) + */ private void discoverInstalledSoftware(Path software_folder) { List subfolders = this.context.getFileAccess().listChildren(software_folder, Files::isDirectory); for (Path subfolder : subfolders) { @@ -115,6 +143,12 @@ private void discoverInstalledSoftware(Path software_folder) { } } + /** + * This method discovers all installed editions of a tool at $IDE_ROOT/_ide/software/default/ and saves them to the edition list of the tool. + * Installed versions of the edition are then recusively discovered + * @param edition_folder The folder where the editions are saved in ($IDE_ROOT/_ide/software/default/) + * @param tool The respective tool for which we are discovering editions + */ private void discoverInstalledEditions(Path edition_folder, IdeTool tool) { List subfolders = this.context.getFileAccess().listChildren(edition_folder, Files::isDirectory); for (Path subfolder : subfolders) { @@ -124,6 +158,11 @@ private void discoverInstalledEditions(Path edition_folder, IdeTool tool) { } } + /** + * This method discovers all installed versions of an edition of a tool at $IDE_ROOT/_ide/software/default// and saves them to the version list of the edition. + * @param version_folder The folder where the versions are saved in ($IDE_ROOT/_ide/software/default//) + * @param edition The respective edition for which we are discovering versions + */ private void discoverInstalledVersions(Path version_folder, IdeToolEdition edition) { List subfolders = this.context.getFileAccess().listChildren(version_folder, Files::isDirectory); for (Path subfolder : subfolders) { @@ -132,20 +171,30 @@ private void discoverInstalledVersions(Path version_folder, IdeToolEdition editi } } + /** + * This method scans the software folder of an IDEasy project for installed tools and matches these against the global tool list created earlier. + * Identified tools are marked as used in the global tool list. + * @param software_folder The software folder of the IDEasy project to scan for used software + * @param project_name The name of the project we are currently scanning + */ private void discoverUsedSoftware(Path software_folder, String project_name) { + // Get all installed tools for this project List subfolders = this.context.getFileAccess().listChildren(software_folder, Files::isDirectory); for (Path current_folder : subfolders) { + // Converts the path of the tool installation to the real path by eliminating symlinks. This allows us to determine whether a tool is installed locally for an IDEasy project or is part + // of the global software installation under $IDE_ROOT/_ide/software/default current_folder = this.context.getFileAccess().toRealPath(current_folder); // Check if directory contains ".ide.software.version" file. If so, read version and add to list of used software if (Files.exists(current_folder.resolve(".ide.software.version"))) { if (!current_folder.startsWith(this.context.getIdeRoot().resolve("_ide/software/default"))) { - // We found a software that is directly installed in an IDEasy project but not in the global software folder. We leave these alone + // We found a software that is locally installed in an IDEasy project but not in the global software folder. We leave these alone. return; } + // Get details of the tool (name, edition, version) String tool_name = current_folder.getParent().getParent().getFileName().toString(); String tool_edition = current_folder.getParent().getFileName().toString(); String tool_version = current_folder.getFileName().toString(); - // Check if software exists in IdeTool list. If so, mark as used + // Check if software exists in global IdeTool list. If so, mark as used for (IdeTool tool : this.installedIdeTools) { if (tool.toolName.equals(tool_name)) { tool.usedBy.add(project_name); @@ -168,6 +217,9 @@ private void discoverUsedSoftware(Path software_folder, String project_name) { } } + /** + * Sets the delete flag for all unused tools, editions, and versions to true. + */ private void markUnusedSoftwareForDeletion() { for (IdeTool tool : this.installedIdeTools) { for (IdeToolEdition edition : tool.editions) { @@ -186,6 +238,10 @@ private void markUnusedSoftwareForDeletion() { } } + /** + * Generates a summary report for tools, editions, and versions to be deleted and prompts the user for confirmation. + * If the user agress, we proceed with deletion of the unused tools, editions, and version. + */ private void logSoftwareToBeDeleted() { String LogOutput = ""; int totalToolsDeleted = 0; @@ -205,6 +261,7 @@ private void logSoftwareToBeDeleted() { } } if (!LogOutputVersion.isBlank()) { + // If at least one version of the edition should be deleted if (versionsDeleted < edition.versions.size()) { LogOutputVersion += "\t\t + " + (edition.versions.size() - versionsDeleted) + " more version(s) of this edition will not be deleted\n"; LogOutputEdition += "\t - " + edition.editionName + "\n" + LogOutputVersion; @@ -214,6 +271,7 @@ private void logSoftwareToBeDeleted() { } } if (!LogOutputEdition.isBlank()) { + // If at least one edition of the tool should have a delete operation if (editionsDeleted < tool.editions.size()) { LogOutputEdition += "\t + " + (tool.editions.size() - editionsDeleted) + " more edition(s) of this tool will not be deleted\n"; } @@ -228,6 +286,7 @@ private void logSoftwareToBeDeleted() { LOG.info("The following installed tools will be deleted: \n" + LogOutput); LOG.info("Summary: {} installed tool versions across {} editions of {} tools will be deleted.", totalVersionsDeleted, totalEditionsDeleted, totalToolsDeleted); + // Ask for conformation. Skipped if --fd flag is provided if (!this.forceDelete.isTrue()) { try { this.context.askToContinue("Do you want to continue?"); @@ -241,6 +300,9 @@ private void logSoftwareToBeDeleted() { } } + /** + * Deletes tools, editions, and versions marked for deletion. + */ private void deleteUnusedSoftware() { int failed_deletion = 0; FileAccess fileAccess = this.context.getFileAccess(); @@ -282,7 +344,8 @@ private void deleteUnusedSoftware() { } } } - + + // Log completion message if (failed_deletion > 0) { LOG.warn("Unused tools have been deleted.\nFailed to delete {} files. Please check the log for details.", failed_deletion); } else { From 533c686aaa0a3f3c92743754e397f6d2d0ab554a Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Wed, 20 May 2026 10:18:58 +0200 Subject: [PATCH 09/27] Minor fixes --- .../devonfw/tools/ide/commandlet/CleanupCommandlet.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index 832155a2bc..c9e8847c3a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -184,11 +184,12 @@ private void discoverUsedSoftware(Path software_folder, String project_name) { // Converts the path of the tool installation to the real path by eliminating symlinks. This allows us to determine whether a tool is installed locally for an IDEasy project or is part // of the global software installation under $IDE_ROOT/_ide/software/default current_folder = this.context.getFileAccess().toRealPath(current_folder); - // Check if directory contains ".ide.software.version" file. If so, read version and add to list of used software - if (Files.exists(current_folder.resolve(".ide.software.version"))) { + // Check if directory contains a software version file. If so, read version and add to list of used software. + if (Files.exists(current_folder.resolve(IdeContext.FILE_SOFTWARE_VERSION)) + || Files.exists(current_folder.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION))) { if (!current_folder.startsWith(this.context.getIdeRoot().resolve("_ide/software/default"))) { // We found a software that is locally installed in an IDEasy project but not in the global software folder. We leave these alone. - return; + continue; } // Get details of the tool (name, edition, version) String tool_name = current_folder.getParent().getParent().getFileName().toString(); @@ -264,8 +265,8 @@ private void logSoftwareToBeDeleted() { // If at least one version of the edition should be deleted if (versionsDeleted < edition.versions.size()) { LogOutputVersion += "\t\t + " + (edition.versions.size() - versionsDeleted) + " more version(s) of this edition will not be deleted\n"; - LogOutputEdition += "\t - " + edition.editionName + "\n" + LogOutputVersion; } + LogOutputEdition += "\t - " + edition.editionName + "\n" + LogOutputVersion; editionsDeleted++; totalEditionsDeleted++; } From bf79fef9bddd9c7f9460e2900b5d4c9b348ca220 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Thu, 21 May 2026 08:42:43 +0200 Subject: [PATCH 10/27] Add basic test --- .../ide/commandlet/CleanupCommandletTest.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java new file mode 100644 index 0000000000..9bdc68b5de --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java @@ -0,0 +1,33 @@ +package com.devonfw.tools.ide.commandlet; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; + +/** + * Test of {@link CleanupCommandlet}. + */ +class CleanupCommandletTest extends AbstractIdeContextTest { + + private static final String PROJECT_BASIC = "basic"; + + /** + * Test of {@link CleanupCommandlet} that {@code az} tool is not used by any project and thus deleted. + */ + @Test + void testCleanupDeletesUnusedGlobalSoftware() { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + CleanupCommandlet cleanup = context.getCommandletManager().getCommandlet(CleanupCommandlet.class); + cleanup.forceDelete.setValue(true); + + // act + cleanup.run(); + + // assert + assertThat(context.getIdeRoot().resolve("_ide/software/default/az")).doesNotExist(); + assertThat(context).logAtSuccess().hasMessage("Unused tools have been deleted successfully."); + } +} From cfb9a1b6e2f0acc2825647b7716f65889b1423e4 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Thu, 21 May 2026 08:49:48 +0200 Subject: [PATCH 11/27] Small optimization --- .../ide/commandlet/CleanupCommandlet.java | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index c9e8847c3a..d64f88170f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -306,41 +306,25 @@ private void logSoftwareToBeDeleted() { */ private void deleteUnusedSoftware() { int failed_deletion = 0; - FileAccess fileAccess = this.context.getFileAccess(); // Delete the tool for (IdeTool tool : this.installedIdeTools) { if (tool.delete) { LOG.debug("Deleting tool {} and all its editions and versions in {}", tool.toolName, tool.path); - try { - fileAccess.delete(tool.path); - } catch (Exception e) { - LOG.error("Failed to delete {}: {}", tool.path, e.getMessage()); - failed_deletion++; - } + failed_deletion += deleteFolder(tool.path); continue; } // Delete editions of the tool for (IdeToolEdition edition : tool.editions) { if (edition.delete) { LOG.debug("Deleting edition {} of tool {} and all its versions in {}", edition.editionName, tool.toolName, edition.path); - try { - fileAccess.delete(edition.path); - } catch (Exception e) { - LOG.error("Failed to delete {}: {}", edition.path, e.getMessage()); - failed_deletion++; - } + failed_deletion += deleteFolder(edition.path); continue; } // Delete versions of the edition for (IdeToolEditionVersion version : edition.versions) { if (version.delete) { LOG.debug("Deleting version {} of edition {} of tool {} in {}", version.versionName, edition.editionName, tool.toolName, version.path); - try { - fileAccess.delete(version.path); - } catch (Exception e) { - LOG.error("Failed to delete {}: {}", version.path, e.getMessage()); - failed_deletion++; - } + failed_deletion += deleteFolder(version.path); } } } @@ -348,9 +332,24 @@ private void deleteUnusedSoftware() { // Log completion message if (failed_deletion > 0) { - LOG.warn("Unused tools have been deleted.\nFailed to delete {} files. Please check the log for details.", failed_deletion); + LOG.warn("Unused tools have been deleted.\nFailed to delete {} tools/editions/versions. Please check the log for details.", failed_deletion); } else { IdeLogLevel.SUCCESS.log(LOG, "Unused tools have been deleted successfully."); } } + + /** + * Deletes a folder at a given path. Logs an error message if unsuccessful. + * @param path The path of the folder to delete + * @return 0 if deletion was successful, 1 if deletion failed + */ + private int deleteFolder(Path path) { + try { + this.context.getFileAccess().delete(path); + } catch (Exception e) { + LOG.error("Failed to delete {}: {}", path, e.getMessage()); + return 1; + } + return 0; + } } \ No newline at end of file From a33342534176c5ddbcec30f8188aa0df05de36f0 Mon Sep 17 00:00:00 2001 From: Alexander Reinicke <167530118+areinicke@users.noreply.github.com> Date: Thu, 21 May 2026 09:06:11 +0200 Subject: [PATCH 12/27] Test commit. No relevant changes --- .../com/devonfw/tools/ide/commandlet/CleanupCommandlet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index d64f88170f..9d91ee9cc0 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -82,7 +82,7 @@ public CleanupCommandlet(IdeContext context) { super(context); addKeyword(getName()); - this.forceDelete = add(new FlagProperty("--fd")); // Force-Delete flag. Skips confirmation prompts if provided + this.forceDelete = add(new FlagProperty("--fd")); // Force-Delete flag: Skips confirmation prompts if provided } @@ -352,4 +352,4 @@ private int deleteFolder(Path path) { } return 0; } -} \ No newline at end of file +} From beeb33d98cf4e9e21e6abecd6e81cf4260425708 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Wed, 27 May 2026 16:32:18 +0200 Subject: [PATCH 13/27] Add fix for ide shell --- .../com/devonfw/tools/ide/commandlet/CleanupCommandlet.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index 9d91ee9cc0..32376e4042 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -9,7 +9,6 @@ import com.devonfw.tools.ide.cli.CliAbortException; import com.devonfw.tools.ide.context.IdeContext; -import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.step.Step; @@ -102,6 +101,9 @@ protected void doRun() { step.run(() -> {discoverAndDeleteUnusedSoftware();}); LOG.debug("Finished cleanup commandlet"); + + // Clear Array Lists so tools are not duplicated when running "ide cleanup" repeatedly in the ide shell + this.installedIdeTools.clear(); } /** From b8a590a1110b1fc872640f61fd2b0d789aaf5234 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Wed, 27 May 2026 16:50:26 +0200 Subject: [PATCH 14/27] Minor adjustment: Move Completion message to end of method --- .../com/devonfw/tools/ide/commandlet/CleanupCommandlet.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index 32376e4042..bb47a01b35 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -100,10 +100,10 @@ protected void doRun() { Step step = context.newStep("Identify and remove unused software"); step.run(() -> {discoverAndDeleteUnusedSoftware();}); - LOG.debug("Finished cleanup commandlet"); - // Clear Array Lists so tools are not duplicated when running "ide cleanup" repeatedly in the ide shell this.installedIdeTools.clear(); + + LOG.debug("Finished cleanup commandlet"); } /** From ed172b1f8349b195c34abaa41c9f26b546b00e94 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:13:07 +0200 Subject: [PATCH 15/27] Refactor Step 1 --- .../ide/commandlet/CleanupCommandlet.java | 564 ++++++++---------- .../cleanupUtil/CleanupIdeTool.java | 105 ++++ .../cleanupUtil/CleanupIdeToolEdition.java | 106 ++++ .../CleanupIdeToolEditionVersion.java | 94 +++ 4 files changed, 570 insertions(+), 299 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java index bb47a01b35..d260af05f9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java @@ -2,356 +2,322 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.devonfw.tools.ide.cli.CliAbortException; +import com.devonfw.tools.ide.commandlet.cleanupUtil.CleanupIdeTool; +import com.devonfw.tools.ide.commandlet.cleanupUtil.CleanupIdeToolEdition; +import com.devonfw.tools.ide.commandlet.cleanupUtil.CleanupIdeToolEditionVersion; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.step.Step; +import com.devonfw.tools.ide.tool.repository.ToolRepository; /** -* Commandlet which scans your IDE installation for unused software (tools not currently used by any project) and removes them. -*/ -public class CleanupCommandlet extends Commandlet{ + * Commandlet which scans your IDE installation for unused software (tools not currently used by any project) and removes them. + */ +public class CleanupCommandlet extends Commandlet { - public final FlagProperty forceDelete; + private static final Logger LOG = LoggerFactory.getLogger(CleanupCommandlet.class); - private static final Logger LOG = LoggerFactory.getLogger(CleanupCommandlet.class); + public final FlagProperty forceDelete; - /** - * Class which represents individual IDE tools. Contains multiple parameter such as the name of the tool, the path where it is installed, - * a list of projects which use this tool and a boolean whether the tool is marked for deletion or not. - * Furthermore, it contains a list of editions of the edition (e.g. for intellij, we could have the editions "intellij" or "ultimate"). - */ - private static class IdeToolEditionVersion { - String versionName; - private final Path path; - private final List usedBy = new java.util.ArrayList<>(); - private boolean delete = false; + public CleanupCommandlet(IdeContext context) { - IdeToolEditionVersion(String versionName, Path path) { - this.versionName = versionName; - this.path = path; - } - } - - /** - * Class which represents individual editions of an IDE tool. Contains multiple parameter such as the name of the edition, the path where it is installed, - * a list of projects which use this edition and a boolean whether the edition is marked for deletion or not. - * Furthermore, it contains a list of versions of the edition (e.g. for intellij ultimate edition, we could have version 2022.3 and 2023.1 installed). - */ - private static class IdeToolEdition { - String editionName; - private final Path path; - private final List usedBy = new java.util.ArrayList<>(); - private boolean delete = false; - private final List versions = new java.util.ArrayList<>(); + super(context); + addKeyword(getName()); + this.forceDelete = add(new FlagProperty("--force-delete")); // Skips confirmation prompts if provided + } - IdeToolEdition(String editionName, Path path) { - this.editionName = editionName; - this.path = path; - } - } - - /** - * Class which represents individual versions of an edition of an IDE tool. Contains multiple parameter such as the name of the version, the path where it is installed, - * a list of projects which use this version and a boolean whether the version is marked for deletion or not. - */ - private static class IdeTool { - String toolName; - private final Path path; - private final List usedBy = new java.util.ArrayList<>(); - private boolean delete = false; - private final List editions = new java.util.ArrayList<>(); + @Override + public String getName() { - IdeTool(String toolName, Path path) { - this.toolName = toolName; - this.path = path; - } - } + return "cleanup"; + } - // List of installed IDE tools in the global software folder at $IDE_ROOT/_ide/software/default. This list is populated at the beginning of the cleanup process and then used to identify unused software and delete it. - private final List installedIdeTools = new java.util.ArrayList<>(); + @Override + protected void doRun() { - public CleanupCommandlet(IdeContext context) { - - super(context); - addKeyword(getName()); - this.forceDelete = add(new FlagProperty("--fd")); // Force-Delete flag: Skips confirmation prompts if provided - } + LOG.debug("Start cleanup commandlet"); - - @Override - public String getName() { - - return "cleanup"; - } - - @Override - protected void doRun() { + List installedCleanupIdeTools = new ArrayList<>(); - LOG.debug("Start cleanup commandlet"); + // Identify and remove unused tools. + Step step = context.newStep("Identify and remove unused software"); + step.run(() -> discoverAndDeleteUnusedSoftware(installedCleanupIdeTools)); - // Identify and remove unused tools. - Step step = context.newStep("Identify and remove unused software"); - step.run(() -> {discoverAndDeleteUnusedSoftware();}); + LOG.debug("Finished cleanup commandlet"); + } - // Clear Array Lists so tools are not duplicated when running "ide cleanup" repeatedly in the ide shell - this.installedIdeTools.clear(); + /** + * This method specifies the primary flow for the discovery of installed and unused software, and its deletion. + * + * @param installedCleanupIdeTools the mutable list of discovered tools, populated and updated during this method's execution. + */ + private void discoverAndDeleteUnusedSoftware(List installedCleanupIdeTools) { + // Iterate over software in $IDE_ROOT/_ide/software/default folder and save installed software to a list + discoverInstalledSoftware(installedCleanupIdeTools, this.context.getSoftwareRepositoryPath().resolve(ToolRepository.ID_DEFAULT)); - LOG.debug("Finished cleanup commandlet"); - } + // Scan for IDEasy projects + List ideasyProjects = this.context.getFileAccess().listChildren(this.context.getIdeRoot(), Files::isDirectory); - /** - * This method specified the primary flow for the discovery of installed and unused software, and its deletion. - */ - private void discoverAndDeleteUnusedSoftware() { - // Iterate over software in $IDE_ROOT/_ide/software folder and save installed software to a list - discoverInstalledSoftware(this.context.getIdeRoot().resolve("_ide/software/default")); - - // Scan for IDEasy projects - List ideasyProjects = this.context.getFileAccess().listChildren(this.context.getIdeRoot(), Files::isDirectory); + // Iterate through IDEasy projects and scan software in software folder. Save found software to list + for (Path ideasyProject : ideasyProjects) { + if (ideasyProject.getFileName().toString().equals("_ide")) { + continue; + } + Path ideasyProjectSoftware = ideasyProject.resolve(IdeContext.FOLDER_SOFTWARE); + discoverUsedSoftware(installedCleanupIdeTools, ideasyProjectSoftware, ideasyProject.getFileName().toString()); + discoverUsedSoftware(installedCleanupIdeTools, ideasyProjectSoftware.resolve(IdeContext.FOLDER_EXTRA), ideasyProject.getFileName().toString()); + } - // Iterate through IDEasy projects and scan software in software folder. Save found software to list - for (Path ideasyProject : ideasyProjects) { - if (ideasyProject.getFileName().toString().equals("_ide")) { - continue; - } - discoverUsedSoftware(ideasyProject.resolve("software"), ideasyProject.getFileName().toString()); - discoverUsedSoftware(ideasyProject.resolve("software/extra"), ideasyProject.getFileName().toString()); - } + // Mark unused software for deletion + markUnusedSoftwareForDeletion(installedCleanupIdeTools); + // Log summary report and proceed with deletion if user confirms + logSoftwareToBeDeleted(installedCleanupIdeTools); + } - // Mark unused software for deletion - markUnusedSoftwareForDeletion(); - // Log summary report and proceed with deletion if user confirms - logSoftwareToBeDeleted(); + /** + * This method discovers all installed tools at $IDE_ROOT/_ide/software/default and saves them to the list of installed tools. + * Installed editions are then recursively discovered. + * + * @param installedCleanupIdeTools the list to populate with discovered tools. + * @param softwareFolder The folder where the software is saved in ($IDE_ROOT/_ide/software/default). + */ + private void discoverInstalledSoftware(List installedCleanupIdeTools, Path softwareFolder) { + List subfolders = this.context.getFileAccess().listChildren(softwareFolder, Files::isDirectory); + for (Path subfolder : subfolders) { + CleanupIdeTool tool = new CleanupIdeTool(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); + installedCleanupIdeTools.add(tool); + discoverInstalledEditions(subfolder, tool); } + } - /** - * This method discovers all installed tools at $IDE_ROOT/_ide/software/default and saves them to the list of installed tools. - * Installed editions are then recusively discovered - * @param software_folder The folder where the software is saved in ($IDE_ROOT/_ide/software/default) - */ - private void discoverInstalledSoftware(Path software_folder) { - List subfolders = this.context.getFileAccess().listChildren(software_folder, Files::isDirectory); - for (Path subfolder : subfolders) { - IdeTool tool = new IdeTool(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); - this.installedIdeTools.add(tool); - discoverInstalledEditions(subfolder, tool); - } + /** + * This method discovers all installed editions of a tool at $IDE_ROOT/_ide/software/default/ and saves them to the edition list of the tool. + * Installed versions of the edition are then recursively discovered. + * + * @param editionFolder The folder where the editions are saved in ($IDE_ROOT/_ide/software/default/). + * @param tool The respective tool for which we are discovering editions. + */ + private void discoverInstalledEditions(Path editionFolder, CleanupIdeTool tool) { + List subfolders = this.context.getFileAccess().listChildren(editionFolder, Files::isDirectory); + for (Path subfolder : subfolders) { + CleanupIdeToolEdition edition = new CleanupIdeToolEdition(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); + tool.getEditions().add(edition); + discoverInstalledVersions(subfolder, edition); } + } - /** - * This method discovers all installed editions of a tool at $IDE_ROOT/_ide/software/default/ and saves them to the edition list of the tool. - * Installed versions of the edition are then recusively discovered - * @param edition_folder The folder where the editions are saved in ($IDE_ROOT/_ide/software/default/) - * @param tool The respective tool for which we are discovering editions - */ - private void discoverInstalledEditions(Path edition_folder, IdeTool tool) { - List subfolders = this.context.getFileAccess().listChildren(edition_folder, Files::isDirectory); - for (Path subfolder : subfolders) { - IdeToolEdition edition = new IdeToolEdition(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); - tool.editions.add(edition); - discoverInstalledVersions(subfolder, edition); - } + /** + * This method discovers all installed versions of an edition of a tool at $IDE_ROOT/_ide/software/default// and saves them to the version list of the edition. + * + * @param versionFolder The folder where the versions are saved in ($IDE_ROOT/_ide/software/default//). + * @param edition The respective edition for which we are discovering versions. + */ + private void discoverInstalledVersions(Path versionFolder, CleanupIdeToolEdition edition) { + List subfolders = this.context.getFileAccess().listChildren(versionFolder, Files::isDirectory); + for (Path subfolder : subfolders) { + CleanupIdeToolEditionVersion version = new CleanupIdeToolEditionVersion(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); + edition.getVersions().add(version); } + } - /** - * This method discovers all installed versions of an edition of a tool at $IDE_ROOT/_ide/software/default// and saves them to the version list of the edition. - * @param version_folder The folder where the versions are saved in ($IDE_ROOT/_ide/software/default//) - * @param edition The respective edition for which we are discovering versions - */ - private void discoverInstalledVersions(Path version_folder, IdeToolEdition edition) { - List subfolders = this.context.getFileAccess().listChildren(version_folder, Files::isDirectory); - for (Path subfolder : subfolders) { - IdeToolEditionVersion version = new IdeToolEditionVersion(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); - edition.versions.add(version); + /** + * This method scans the software folder of an IDEasy project for installed tools and matches these against the global tool list created earlier. + * Identified tools are marked as used in the global tool list. + * + * @param installedCleanupIdeTools the list of installed tools to check against. + * @param softwareFolder The software folder of the IDEasy project to scan for used software. + * @param projectName The name of the project we are currently scanning. + */ + private void discoverUsedSoftware(List installedCleanupIdeTools, Path softwareFolder, String projectName) { + // Get all installed tools for this project + List subfolders = this.context.getFileAccess().listChildren(softwareFolder, Files::isDirectory); + for (Path currentFolder : subfolders) { + // Converts the path of the tool installation to the real path by eliminating symlinks. This allows us to determine whether a tool is installed locally for an IDEasy project or is part + // of the global software installation under $IDE_ROOT/_ide/software/default + currentFolder = this.context.getFileAccess().toRealPath(currentFolder); + // Check if directory contains a software version file. If so, read version and add to list of used software. + if (Files.exists(currentFolder.resolve(IdeContext.FILE_SOFTWARE_VERSION)) + || Files.exists(currentFolder.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION))) { + if (!currentFolder.startsWith(this.context.getSoftwareRepositoryPath().resolve(ToolRepository.ID_DEFAULT))) { + // We found a software that is locally installed in an IDEasy project but not in the global software folder. We leave these alone. + continue; } - } - - /** - * This method scans the software folder of an IDEasy project for installed tools and matches these against the global tool list created earlier. - * Identified tools are marked as used in the global tool list. - * @param software_folder The software folder of the IDEasy project to scan for used software - * @param project_name The name of the project we are currently scanning - */ - private void discoverUsedSoftware(Path software_folder, String project_name) { - // Get all installed tools for this project - List subfolders = this.context.getFileAccess().listChildren(software_folder, Files::isDirectory); - for (Path current_folder : subfolders) { - // Converts the path of the tool installation to the real path by eliminating symlinks. This allows us to determine whether a tool is installed locally for an IDEasy project or is part - // of the global software installation under $IDE_ROOT/_ide/software/default - current_folder = this.context.getFileAccess().toRealPath(current_folder); - // Check if directory contains a software version file. If so, read version and add to list of used software. - if (Files.exists(current_folder.resolve(IdeContext.FILE_SOFTWARE_VERSION)) - || Files.exists(current_folder.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION))) { - if (!current_folder.startsWith(this.context.getIdeRoot().resolve("_ide/software/default"))) { - // We found a software that is locally installed in an IDEasy project but not in the global software folder. We leave these alone. - continue; - } - // Get details of the tool (name, edition, version) - String tool_name = current_folder.getParent().getParent().getFileName().toString(); - String tool_edition = current_folder.getParent().getFileName().toString(); - String tool_version = current_folder.getFileName().toString(); - // Check if software exists in global IdeTool list. If so, mark as used - for (IdeTool tool : this.installedIdeTools) { - if (tool.toolName.equals(tool_name)) { - tool.usedBy.add(project_name); - for (IdeToolEdition edition : tool.editions) { - if (edition.editionName.equals(tool_edition)) { - edition.usedBy.add(project_name); - for (IdeToolEditionVersion version : edition.versions) { - if (version.versionName.equals(tool_version)) { - version.usedBy.add(project_name); - break; - } - } - break; - } - } - break; - } + // Get details of the tool (name, edition, version) + // currentFolder has the structure «repo-path»/«tool»/«edition»/«version» + String toolVersion = currentFolder.getFileName().toString(); + Path toolEditionFolder = currentFolder.getParent(); + String toolEdition = toolEditionFolder.getFileName().toString(); + String toolName = toolEditionFolder.getParent().getFileName().toString(); + // Check if software exists in global CleanupIdeTool list. If so, mark as used + for (CleanupIdeTool tool : installedCleanupIdeTools) { + if (tool.toolName.equals(toolName)) { + tool.getUsedBy().add(projectName); + for (CleanupIdeToolEdition edition : tool.getEditions()) { + if (edition.editionName.equals(toolEdition)) { + edition.getUsedBy().add(projectName); + for (CleanupIdeToolEditionVersion version : edition.getVersions()) { + if (version.versionName.equals(toolVersion)) { + version.getUsedBy().add(projectName); + break; + } } + break; + } } + break; + } } + } } + } - /** - * Sets the delete flag for all unused tools, editions, and versions to true. - */ - private void markUnusedSoftwareForDeletion() { - for (IdeTool tool : this.installedIdeTools) { - for (IdeToolEdition edition : tool.editions) { - for (IdeToolEditionVersion version : edition.versions) { - if (version.usedBy.isEmpty()) { - version.delete = true; - } - } - if (edition.usedBy.isEmpty()) { - edition.delete = true; - } - } - if (tool.usedBy.isEmpty()) { - tool.delete = true; - } + /** + * Sets the delete flag for all unused tools, editions, and versions to true. + * + * @param installedCleanupIdeTools the list of installed tools to mark. + */ + private void markUnusedSoftwareForDeletion(List installedCleanupIdeTools) { + for (CleanupIdeTool tool : installedCleanupIdeTools) { + for (CleanupIdeToolEdition edition : tool.getEditions()) { + for (CleanupIdeToolEditionVersion version : edition.getVersions()) { + if (version.isUnused()) { + version.setDelete(true); + } } + if (edition.isUnused()) { + edition.setDelete(true); + } + } + if (tool.isUnused()) { + tool.setDelete(true); + } } + } - /** - * Generates a summary report for tools, editions, and versions to be deleted and prompts the user for confirmation. - * If the user agress, we proceed with deletion of the unused tools, editions, and version. - */ - private void logSoftwareToBeDeleted() { - String LogOutput = ""; - int totalToolsDeleted = 0; - int totalEditionsDeleted = 0; - int totalVersionsDeleted = 0; - for (IdeTool tool : this.installedIdeTools) { - String LogOutputEdition = ""; - int editionsDeleted = 0; - for (IdeToolEdition edition : tool.editions) { - String LogOutputVersion = ""; - int versionsDeleted = 0; - for (IdeToolEditionVersion version : edition.versions) { - if (version.delete) { - LogOutputVersion += "\t\t - " + version.versionName + "\n"; - versionsDeleted++; - totalVersionsDeleted++; - } - } - if (!LogOutputVersion.isBlank()) { - // If at least one version of the edition should be deleted - if (versionsDeleted < edition.versions.size()) { - LogOutputVersion += "\t\t + " + (edition.versions.size() - versionsDeleted) + " more version(s) of this edition will not be deleted\n"; - } - LogOutputEdition += "\t - " + edition.editionName + "\n" + LogOutputVersion; - editionsDeleted++; - totalEditionsDeleted++; - } - } - if (!LogOutputEdition.isBlank()) { - // If at least one edition of the tool should have a delete operation - if (editionsDeleted < tool.editions.size()) { - LogOutputEdition += "\t + " + (tool.editions.size() - editionsDeleted) + " more edition(s) of this tool will not be deleted\n"; - } - LogOutput += " - " + tool.toolName + "\n" + LogOutputEdition; - totalToolsDeleted++; - } + /** + * Generates a summary report for tools, editions, and versions to be deleted and prompts the user for confirmation. + * If the user agrees, we proceed with deletion of the unused tools, editions, and versions. + * + * @param installedCleanupIdeTools the list of installed tools with deletion flags set. + */ + private void logSoftwareToBeDeleted(List installedCleanupIdeTools) { + String logOutput = ""; + int totalToolsDeleted = 0; + int totalEditionsDeleted = 0; + int totalVersionsDeleted = 0; + for (CleanupIdeTool tool : installedCleanupIdeTools) { + String logOutputEdition = ""; + int editionsDeleted = 0; + for (CleanupIdeToolEdition edition : tool.getEditions()) { + String logOutputVersion = ""; + int versionsDeleted = 0; + for (CleanupIdeToolEditionVersion version : edition.getVersions()) { + if (version.isDelete()) { + logOutputVersion += "\t\t - " + version.versionName + "\n"; + versionsDeleted++; + totalVersionsDeleted++; + } } + if (!logOutputVersion.isBlank()) { + // If at least one version of the edition should be deleted + if (versionsDeleted < edition.getVersions().size()) { + logOutputVersion += "\t\t + " + (edition.getVersions().size() - versionsDeleted) + " more version(s) of this edition will not be deleted\n"; + } + logOutputEdition += "\t - " + edition.editionName + "\n" + logOutputVersion; + editionsDeleted++; + totalEditionsDeleted++; + } + } + if (!logOutputEdition.isBlank()) { + // If at least one edition of the tool should have a delete operation + if (editionsDeleted < tool.getEditions().size()) { + logOutputEdition += "\t + " + (tool.getEditions().size() - editionsDeleted) + " more edition(s) of this tool will not be deleted\n"; + } + logOutput += " - " + tool.toolName + "\n" + logOutputEdition; + totalToolsDeleted++; + } + } - if (LogOutput.isBlank()) { - LOG.info("No installed tools will be deleted. All installed software is used by at least one project."); - } else { - LOG.info("The following installed tools will be deleted: \n" + LogOutput); - LOG.info("Summary: {} installed tool versions across {} editions of {} tools will be deleted.", totalVersionsDeleted, totalEditionsDeleted, totalToolsDeleted); - - // Ask for conformation. Skipped if --fd flag is provided - if (!this.forceDelete.isTrue()) { - try { - this.context.askToContinue("Do you want to continue?"); - - } catch (CliAbortException e) { - LOG.info("Installed Tools will not be deleted."); - return; - } - } - deleteUnusedSoftware(); + if (logOutput.isBlank()) { + LOG.info("No installed tools will be deleted. All installed software is used by at least one project."); + } else { + LOG.info("The following installed tools will be deleted: \n" + logOutput); + LOG.info("Summary: {} installed tool versions across {} editions of {} tools will be deleted.", totalVersionsDeleted, totalEditionsDeleted, totalToolsDeleted); + + // Ask for confirmation. Skipped if --force-delete flag is provided + if (!this.forceDelete.isTrue()) { + try { + this.context.askToContinue("Do you want to continue?"); + + } catch (CliAbortException e) { + LOG.info("Installed Tools will not be deleted."); + return; } + } + deleteUnusedSoftware(installedCleanupIdeTools); } + } - /** - * Deletes tools, editions, and versions marked for deletion. - */ - private void deleteUnusedSoftware() { - int failed_deletion = 0; - // Delete the tool - for (IdeTool tool : this.installedIdeTools) { - if (tool.delete) { - LOG.debug("Deleting tool {} and all its editions and versions in {}", tool.toolName, tool.path); - failed_deletion += deleteFolder(tool.path); - continue; - } - // Delete editions of the tool - for (IdeToolEdition edition : tool.editions) { - if (edition.delete) { - LOG.debug("Deleting edition {} of tool {} and all its versions in {}", edition.editionName, tool.toolName, edition.path); - failed_deletion += deleteFolder(edition.path); - continue; - } - // Delete versions of the edition - for (IdeToolEditionVersion version : edition.versions) { - if (version.delete) { - LOG.debug("Deleting version {} of edition {} of tool {} in {}", version.versionName, edition.editionName, tool.toolName, version.path); - failed_deletion += deleteFolder(version.path); - } - } - } + /** + * Deletes tools, editions, and versions marked for deletion. + * + * @param installedCleanupIdeTools the list of installed tools to delete from. + */ + private void deleteUnusedSoftware(List installedCleanupIdeTools) { + int failedDeletion = 0; + // Delete the tool + for (CleanupIdeTool tool : installedCleanupIdeTools) { + if (tool.isDelete()) { + LOG.debug("Deleting tool {} and all its editions and versions in {}", tool.toolName, tool.getPath()); + failedDeletion += deleteFolder(tool.getPath()); + continue; + } + // Delete editions of the tool + for (CleanupIdeToolEdition edition : tool.getEditions()) { + if (edition.isDelete()) { + LOG.debug("Deleting edition {} of tool {} and all its versions in {}", edition.editionName, tool.toolName, edition.getPath()); + failedDeletion += deleteFolder(edition.getPath()); + continue; } - - // Log completion message - if (failed_deletion > 0) { - LOG.warn("Unused tools have been deleted.\nFailed to delete {} tools/editions/versions. Please check the log for details.", failed_deletion); - } else { - IdeLogLevel.SUCCESS.log(LOG, "Unused tools have been deleted successfully."); + // Delete versions of the edition + for (CleanupIdeToolEditionVersion version : edition.getVersions()) { + if (version.isDelete()) { + LOG.debug("Deleting version {} of edition {} of tool {} in {}", version.versionName, edition.editionName, tool.toolName, version.getPath()); + failedDeletion += deleteFolder(version.getPath()); + } } + } } - /** - * Deletes a folder at a given path. Logs an error message if unsuccessful. - * @param path The path of the folder to delete - * @return 0 if deletion was successful, 1 if deletion failed - */ - private int deleteFolder(Path path) { - try { - this.context.getFileAccess().delete(path); - } catch (Exception e) { - LOG.error("Failed to delete {}: {}", path, e.getMessage()); - return 1; - } - return 0; + // Log completion message + if (failedDeletion > 0) { + LOG.warn("Unused tools have been deleted.\nFailed to delete {} tools/editions/versions. Please check the log for details.", failedDeletion); + } else { + IdeLogLevel.SUCCESS.log(LOG, "Unused tools have been deleted successfully."); + } + } + + /** + * Deletes a folder at a given path. Logs an error message if unsuccessful. + * + * @param path The path of the folder to delete. + * @return 0 if deletion was successful, 1 if deletion failed. + */ + private int deleteFolder(Path path) { + try { + this.context.getFileAccess().delete(path); + } catch (Exception e) { + LOG.error("Failed to delete {}: {}", path, e.getMessage()); + return 1; } + return 0; + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java new file mode 100644 index 0000000000..1660f3af64 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java @@ -0,0 +1,105 @@ +package com.devonfw.tools.ide.commandlet.cleanupUtil; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an installed IDE tool in the global software folder as discovered by the {@code cleanup} commandlet. + *

+ * Contains the tool name, installation path, a list of projects that use this tool, + * and a flag indicating whether the tool is marked for deletion. This class also + * holds a list of {@link CleanupIdeToolEdition editions} belonging to this tool. + */ +public class CleanupIdeTool { + + /** The name of this tool. */ + public final String toolName; + + private final Path path; + private final List usedBy; + private boolean delete; + + private final List editions; + + /** + * Constructor. + * + * @param toolName the name of the tool. + * @param path the installation {@link Path} of this tool. + */ + public CleanupIdeTool(String toolName, Path path) { + + this.toolName = toolName; + this.path = path; + this.usedBy = new ArrayList<>(); + this.delete = false; + this.editions = new ArrayList<>(); + } + + /** + * @return the installation {@link Path} of this tool. + */ + public Path getPath() { + + return this.path; + } + + /** + * @return the list of project names that currently use this tool. + */ + public List getUsedBy() { + + return this.usedBy; + } + + /** + * Marks this tool as used by the given project. + * + * @param projectName the name of the project that uses this tool. + */ + public void markUsedBy(String projectName) { + + this.usedBy.add(projectName); + } + + /** + * @return {@code true} if this tool is marked for deletion. + */ + public boolean isDelete() { + + return this.delete; + } + + /** + * Sets the deletion flag. + * + * @param delete {@code true} to mark this tool for deletion. + */ + public void setDelete(boolean delete) { + + this.delete = delete; + } + + /** + * @return {@code true} if no project currently uses this tool. + */ + public boolean isUnused() { + + return this.usedBy.isEmpty(); + } + + /** + * @return the list of {@link CleanupIdeToolEdition editions} belonging to this tool. + */ + public List getEditions() { + + return this.editions; + } + + @Override + public String toString() { + + return "CleanupIdeTool[" + this.toolName + "]"; + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java new file mode 100644 index 0000000000..7f67468436 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java @@ -0,0 +1,106 @@ +package com.devonfw.tools.ide.commandlet.cleanupUtil; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an edition of an IDE tool in the global software folder as discovered by the {@code cleanup} commandlet. + *

+ * For example, for IntelliJ, the editions could be "community" and "ultimate". This class + * contains the edition name, the installation path, a list of projects that use this + * edition, and a flag indicating whether the edition is marked for deletion. It also holds + * a list of {@link CleanupIdeToolEditionVersion versions} belonging to this edition. + */ +public class CleanupIdeToolEdition { + + /** The name of this edition. */ + public final String editionName; + + private final Path path; + private final List usedBy; + private boolean delete; + + private final List versions; + + /** + * Constructor. + * + * @param editionName the name of the edition. + * @param path the installation {@link Path} of this edition. + */ + public CleanupIdeToolEdition(String editionName, Path path) { + + this.editionName = editionName; + this.path = path; + this.usedBy = new ArrayList<>(); + this.delete = false; + this.versions = new ArrayList<>(); + } + + /** + * @return the installation {@link Path} of this edition. + */ + public Path getPath() { + + return this.path; + } + + /** + * @return the list of project names that currently use this edition. + */ + public List getUsedBy() { + + return this.usedBy; + } + + /** + * Marks this edition as used by the given project. + * + * @param projectName the name of the project using this edition. + */ + public void addUsedBy(String projectName) { + + this.usedBy.add(projectName); + } + + /** + * @return {@code true} if this edition is marked for deletion. + */ + public boolean isDelete() { + + return this.delete; + } + + /** + * Sets the deletion flag. + * + * @param delete {@code true} to mark this edition for deletion. + */ + public void setDelete(boolean delete) { + + this.delete = delete; + } + + /** + * @return {@code true} if no project currently uses this edition. + */ + public boolean isUnused() { + + return this.usedBy.isEmpty(); + } + + /** + * @return the list of {@link CleanupIdeToolEditionVersion versions} belonging to this edition. + */ + public List getVersions() { + + return this.versions; + } + + @Override + public String toString() { + + return "CleanupIdeToolEdition[" + this.editionName + "]"; + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java new file mode 100644 index 0000000000..75b6d04ebf --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java @@ -0,0 +1,94 @@ +package com.devonfw.tools.ide.commandlet.cleanupUtil; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a version of an IDE tool edition in the global software folder as discovered by the {@code cleanup} commandlet. + *

+ * For example, for IntelliJ's "ultimate" edition, versions could be "2022.3" and "2023.1". + * This class contains the version name, the installation path, a list of projects that + * use this version, and a flag indicating whether the version is marked for deletion. + */ +public class CleanupIdeToolEditionVersion { + + /** The name of this version. */ + public final String versionName; + + private final Path path; + private final List usedBy; + private boolean delete; + + /** + * Constructor. + * + * @param versionName the name of the version. + * @param path the installation {@link Path} of this version. + */ + public CleanupIdeToolEditionVersion(String versionName, Path path) { + + this.versionName = versionName; + this.path = path; + this.usedBy = new ArrayList<>(); + this.delete = false; + } + + /** + * @return the installation {@link Path} of this version. + */ + public Path getPath() { + + return this.path; + } + + /** + * @return the list of project names that currently use this version. + */ + public List getUsedBy() { + + return this.usedBy; + } + + /** + * Marks this version as used by the given project. + * + * @param projectName the name of the project using this version. + */ + public void addUsedBy(String projectName) { + + this.usedBy.add(projectName); + } + + /** + * @return {@code true} if this version is marked for deletion. + */ + public boolean isDelete() { + + return this.delete; + } + + /** + * Sets the deletion flag. + * + * @param delete {@code true} to mark this version for deletion. + */ + public void setDelete(boolean delete) { + + this.delete = delete; + } + + /** + * @return {@code true} if no project currently uses this version. + */ + public boolean isUnused() { + + return this.usedBy.isEmpty(); + } + + @Override + public String toString() { + + return "CleanupIdeToolEditionVersion[" + this.versionName + "]"; + } +} From 17ded2862c8863727a5b1d817bc8bc7e90aa9970 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Mon, 15 Jun 2026 08:35:48 +0200 Subject: [PATCH 16/27] Add additional JavaDoc --- .../tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java | 6 ++++++ .../ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java | 6 ++++++ .../cleanupUtil/CleanupIdeToolEditionVersion.java | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java index 1660f3af64..b715c6b909 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java @@ -16,10 +16,16 @@ public class CleanupIdeTool { /** The name of this tool. */ public final String toolName; + /** The installation {@link Path} of this tool. */ private final Path path; + + /** A list of project names that currently use this tool. */ private final List usedBy; + + /** A flag indicating whether the tool is marked for deletion. */ private boolean delete; + /** A list of {@link CleanupIdeToolEdition editions} belonging to this tool. */ private final List editions; /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java index 7f67468436..12b12910cf 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java @@ -17,10 +17,16 @@ public class CleanupIdeToolEdition { /** The name of this edition. */ public final String editionName; + /** The installation {@link Path} of this edition. */ private final Path path; + + /** A list of project names that currently use this edition. */ private final List usedBy; + + /** A flag indicating whether the edition is marked for deletion. */ private boolean delete; + /** A list of {@link CleanupIdeToolEditionVersion versions} belonging to this edition. */ private final List versions; /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java index 75b6d04ebf..e7c83b5b11 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java @@ -16,8 +16,13 @@ public class CleanupIdeToolEditionVersion { /** The name of this version. */ public final String versionName; + /** The installation {@link Path} of this version. */ private final Path path; + + /** A list of project names that currently use this version. */ private final List usedBy; + + /** A flag indicating whether the version is marked for deletion. */ private boolean delete; /** From 432c81eccdb185552f5e8653156d73701f642cb8 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Mon, 15 Jun 2026 08:43:44 +0200 Subject: [PATCH 17/27] Update help files --- cli/src/main/resources/nls/Help.properties | 2 +- cli/src/main/resources/nls/Help_de.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 957646a530..45be7bbbab 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -161,7 +161,7 @@ icd-hint=Hint: Use 'icd' command to easily navigate between your IDE home, proje opt.--batch=enable batch mode (non-interactive). opt.--code=clone given code repository containing a settings folder into workspaces so that settings can be committed alongside code changes. opt.--debug=enable debug logging. -opt.--fd=Delete tools without confirmation +opt.--force-delete=Delete tools without confirmation opt.--force=enable force mode. opt.--force-plugin-reinstall=resets installed plugins to the project configuration opt.--force-plugins=force plugin (re)installation. diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index fd4a8272eb..c9cdf0b4d9 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -161,7 +161,7 @@ icd-hint=Hinweis: Verwenden Sie den Befehl 'icd' um einfach zwischen Ihrem IDE-H opt.--batch=Aktiviert den Batch-Modus (nicht-interaktive Stapelverarbeitung). opt.--code=Git-Repository sowohl als Code- als auch als Settings-Repository verwenden. opt.--debug=Aktiviert Debug-Ausgaben (Fehleranalyse). -opt.--fd=Ungenutzte Werkzeuge ohne Bestätigung löschen. +opt.--force-delete=Ungenutzte Werkzeuge ohne Bestätigung löschen. opt.--force=Aktiviert den Force-Modus (Erzwingen). opt.--force-plugin-reinstall=Setzt installierte Plugins zurück auf die Projektkonfiguration. opt.--force-plugins=Erzwingt die (Re)Installation von Plugins. From 8a6b88d2e971f39502c985f765c1c0ed2eca6bc3 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Mon, 15 Jun 2026 08:44:35 +0200 Subject: [PATCH 18/27] Update Help files --- cli/src/main/resources/nls/Help.properties | 2 +- cli/src/main/resources/nls/Help_de.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 45be7bbbab..8bdc9ce4af 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -161,8 +161,8 @@ icd-hint=Hint: Use 'icd' command to easily navigate between your IDE home, proje opt.--batch=enable batch mode (non-interactive). opt.--code=clone given code repository containing a settings folder into workspaces so that settings can be committed alongside code changes. opt.--debug=enable debug logging. -opt.--force-delete=Delete tools without confirmation opt.--force=enable force mode. +opt.--force-delete=Delete tools without confirmation opt.--force-plugin-reinstall=resets installed plugins to the project configuration opt.--force-plugins=force plugin (re)installation. opt.--force-pull=force pull of settings even for code repository. diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index c9cdf0b4d9..fa3487e42e 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -161,8 +161,8 @@ icd-hint=Hinweis: Verwenden Sie den Befehl 'icd' um einfach zwischen Ihrem IDE-H opt.--batch=Aktiviert den Batch-Modus (nicht-interaktive Stapelverarbeitung). opt.--code=Git-Repository sowohl als Code- als auch als Settings-Repository verwenden. opt.--debug=Aktiviert Debug-Ausgaben (Fehleranalyse). -opt.--force-delete=Ungenutzte Werkzeuge ohne Bestätigung löschen. opt.--force=Aktiviert den Force-Modus (Erzwingen). +opt.--force-delete=Ungenutzte Werkzeuge ohne Bestätigung löschen. opt.--force-plugin-reinstall=Setzt installierte Plugins zurück auf die Projektkonfiguration. opt.--force-plugins=Erzwingt die (Re)Installation von Plugins. opt.--force-pull=Erzwingt einen Pull der settings auch im Fall eines Code-Repositories. From ca229d20e48e05a73f6905e783a8124bf764dfb6 Mon Sep 17 00:00:00 2001 From: areinicke <167530118+areinicke@users.noreply.github.com> Date: Mon, 6 Jul 2026 08:55:56 +0200 Subject: [PATCH 19/27] Change package name and move cleanup commandlet into package --- .../devonfw/tools/ide/commandlet/CommandletManagerImpl.java | 1 + .../ide/commandlet/{ => cleanup}/CleanupCommandlet.java | 6 ++---- .../commandlet/{cleanupUtil => cleanup}/CleanupIdeTool.java | 2 +- .../{cleanupUtil => cleanup}/CleanupIdeToolEdition.java | 2 +- .../CleanupIdeToolEditionVersion.java | 2 +- .../devonfw/tools/ide/commandlet/CleanupCommandletTest.java | 1 + 6 files changed, 7 insertions(+), 7 deletions(-) rename cli/src/main/java/com/devonfw/tools/ide/commandlet/{ => cleanup}/CleanupCommandlet.java (98%) rename cli/src/main/java/com/devonfw/tools/ide/commandlet/{cleanupUtil => cleanup}/CleanupIdeTool.java (97%) rename cli/src/main/java/com/devonfw/tools/ide/commandlet/{cleanupUtil => cleanup}/CleanupIdeToolEdition.java (98%) rename cli/src/main/java/com/devonfw/tools/ide/commandlet/{cleanupUtil => cleanup}/CleanupIdeToolEditionVersion.java (97%) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index 8f8ee63d6b..6d12cc6c16 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -15,6 +15,7 @@ import com.devonfw.tools.ide.cli.CliArgument; import com.devonfw.tools.ide.cli.CliArguments; +import com.devonfw.tools.ide.commandlet.cleanup.CleanupCommandlet; import com.devonfw.tools.ide.completion.CompletionCandidateCollector; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.git.repository.RepositoryCommandlet; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java similarity index 98% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java index d260af05f9..b43ccab996 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.commandlet; +package com.devonfw.tools.ide.commandlet.cleanup; import java.nio.file.Files; import java.nio.file.Path; @@ -9,9 +9,7 @@ import org.slf4j.LoggerFactory; import com.devonfw.tools.ide.cli.CliAbortException; -import com.devonfw.tools.ide.commandlet.cleanupUtil.CleanupIdeTool; -import com.devonfw.tools.ide.commandlet.cleanupUtil.CleanupIdeToolEdition; -import com.devonfw.tools.ide.commandlet.cleanupUtil.CleanupIdeToolEditionVersion; +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.FlagProperty; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java similarity index 97% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java index b715c6b909..d7e55cd216 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeTool.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.commandlet.cleanupUtil; +package com.devonfw.tools.ide.commandlet.cleanup; import java.nio.file.Path; import java.util.ArrayList; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java similarity index 98% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java index 12b12910cf..06e79a03b3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEdition.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.commandlet.cleanupUtil; +package com.devonfw.tools.ide.commandlet.cleanup; import java.nio.file.Path; import java.util.ArrayList; diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEditionVersion.java similarity index 97% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEditionVersion.java index e7c83b5b11..e4ceada3ad 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanupUtil/CleanupIdeToolEditionVersion.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEditionVersion.java @@ -1,4 +1,4 @@ -package com.devonfw.tools.ide.commandlet.cleanupUtil; +package com.devonfw.tools.ide.commandlet.cleanup; import java.nio.file.Path; import java.util.ArrayList; diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java index 9bdc68b5de..ae2373ade8 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; +import com.devonfw.tools.ide.commandlet.cleanup.CleanupCommandlet; import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeTestContext; From 816c8277181c56a4722856f666100b628b4113b1 Mon Sep 17 00:00:00 2001 From: caylipp Date: Wed, 29 Jul 2026 13:20:56 +0200 Subject: [PATCH 20/27] #1953: Complete cleanup commandlet review - scan all global software repositories - detect used versions by resolved installation paths - support links to installation subfolders and nested extra tools - make cleanup discovery stateless - track project usage only on version level - reuse the global force mode for confirmation - add regression tests for used and unused installations --- .../commandlet/cleanup/CleanupCommandlet.java | 157 +++++++++------- .../commandlet/cleanup/CleanupIdeTool.java | 29 +-- .../cleanup/CleanupIdeToolEdition.java | 30 +-- .../cleanup/CleanupIdeToolEditionVersion.java | 4 +- cli/src/main/resources/nls/Help.properties | 3 +- cli/src/main/resources/nls/Help_de.properties | 1 - .../ide/commandlet/CleanupCommandletTest.java | 177 +++++++++++++++++- 7 files changed, 269 insertions(+), 132 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java index b43ccab996..cbf74b1209 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java @@ -12,9 +12,7 @@ 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.FlagProperty; import com.devonfw.tools.ide.step.Step; -import com.devonfw.tools.ide.tool.repository.ToolRepository; /** * Commandlet which scans your IDE installation for unused software (tools not currently used by any project) and removes them. @@ -23,13 +21,10 @@ public class CleanupCommandlet extends Commandlet { private static final Logger LOG = LoggerFactory.getLogger(CleanupCommandlet.class); - public final FlagProperty forceDelete; - public CleanupCommandlet(IdeContext context) { super(context); addKeyword(getName()); - this.forceDelete = add(new FlagProperty("--force-delete")); // Skips confirmation prompts if provided } @Override @@ -43,23 +38,19 @@ protected void doRun() { LOG.debug("Start cleanup commandlet"); - List installedCleanupIdeTools = new ArrayList<>(); - // Identify and remove unused tools. Step step = context.newStep("Identify and remove unused software"); - step.run(() -> discoverAndDeleteUnusedSoftware(installedCleanupIdeTools)); + step.run(this::discoverAndDeleteUnusedSoftware); LOG.debug("Finished cleanup commandlet"); } /** * This method specifies the primary flow for the discovery of installed and unused software, and its deletion. - * - * @param installedCleanupIdeTools the mutable list of discovered tools, populated and updated during this method's execution. */ - private void discoverAndDeleteUnusedSoftware(List installedCleanupIdeTools) { - // Iterate over software in $IDE_ROOT/_ide/software/default folder and save installed software to a list - discoverInstalledSoftware(installedCleanupIdeTools, this.context.getSoftwareRepositoryPath().resolve(ToolRepository.ID_DEFAULT)); + private void discoverAndDeleteUnusedSoftware() { + // Iterate over software in $IDE_ROOT/_ide/software repositories and save installed software to a list + List installedCleanupIdeTools = discoverInstalledSoftware(); // Scan for IDEasy projects List ideasyProjects = this.context.getFileAccess().listChildren(this.context.getIdeRoot(), Files::isDirectory); @@ -81,26 +72,40 @@ private void discoverAndDeleteUnusedSoftware(List installedClean } /** - * This method discovers all installed tools at $IDE_ROOT/_ide/software/default and saves them to the list of installed tools. - * Installed editions are then recursively discovered. + * This method discovers all installed tools in all repositories below $IDE_ROOT/_ide/software and saves them to the list of installed tools. + * + * @return the list of discovered installed tools. + */ + private List discoverInstalledSoftware() { + List installedCleanupIdeTools = new ArrayList<>(); + Path softwareRepositoryPath = this.context.getSoftwareRepositoryPath(); + List repositoryFolders = this.context.getFileAccess().listChildren(softwareRepositoryPath, Files::isDirectory); + for (Path repositoryFolder : repositoryFolders) { + discoverInstalledSoftwareRepository(installedCleanupIdeTools, repositoryFolder); + } + return installedCleanupIdeTools; + } + + /** + * This method discovers all installed tools in one software repository. Installed editions are then recursively discovered. * * @param installedCleanupIdeTools the list to populate with discovered tools. - * @param softwareFolder The folder where the software is saved in ($IDE_ROOT/_ide/software/default). + * @param repositoryFolder The software repository folder to scan. */ - private void discoverInstalledSoftware(List installedCleanupIdeTools, Path softwareFolder) { - List subfolders = this.context.getFileAccess().listChildren(softwareFolder, Files::isDirectory); - for (Path subfolder : subfolders) { - CleanupIdeTool tool = new CleanupIdeTool(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); + private void discoverInstalledSoftwareRepository(List installedCleanupIdeTools, Path repositoryFolder) { + List toolFolders = this.context.getFileAccess().listChildren(repositoryFolder, Files::isDirectory); + for (Path toolFolder : toolFolders) { + CleanupIdeTool tool = new CleanupIdeTool(toolFolder.getFileName().toString(), this.context.getFileAccess().toRealPath(toolFolder)); installedCleanupIdeTools.add(tool); - discoverInstalledEditions(subfolder, tool); + discoverInstalledEditions(toolFolder, tool); } } /** - * This method discovers all installed editions of a tool at $IDE_ROOT/_ide/software/default/ and saves them to the edition list of the tool. + * This method discovers all installed editions of a tool at $IDE_ROOT/_ide/software// and saves them to the edition list of the tool. * Installed versions of the edition are then recursively discovered. * - * @param editionFolder The folder where the editions are saved in ($IDE_ROOT/_ide/software/default/). + * @param editionFolder The folder where the editions are saved in ($IDE_ROOT/_ide/software//). * @param tool The respective tool for which we are discovering editions. */ private void discoverInstalledEditions(Path editionFolder, CleanupIdeTool tool) { @@ -113,68 +118,79 @@ private void discoverInstalledEditions(Path editionFolder, CleanupIdeTool tool) } /** - * This method discovers all installed versions of an edition of a tool at $IDE_ROOT/_ide/software/default// and saves them to the version list of the edition. + * This method discovers all installed versions of an edition of a tool at $IDE_ROOT/_ide/software/// and saves them to the version + * list of the edition. * - * @param versionFolder The folder where the versions are saved in ($IDE_ROOT/_ide/software/default//). + * @param versionFolder The folder where the versions are saved in ($IDE_ROOT/_ide/software///). * @param edition The respective edition for which we are discovering versions. */ private void discoverInstalledVersions(Path versionFolder, CleanupIdeToolEdition edition) { List subfolders = this.context.getFileAccess().listChildren(versionFolder, Files::isDirectory); for (Path subfolder : subfolders) { - CleanupIdeToolEditionVersion version = new CleanupIdeToolEditionVersion(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); + CleanupIdeToolEditionVersion version = new CleanupIdeToolEditionVersion(subfolder.getFileName().toString(), + this.context.getFileAccess().toRealPath(subfolder)); edition.getVersions().add(version); } } /** - * This method scans the software folder of an IDEasy project for installed tools and matches these against the global tool list created earlier. - * Identified tools are marked as used in the global tool list. + * This method scans the software folder of an IDEasy project for installed tools and matches these against the global tool list created earlier. Identified + * tools are marked as used in the global tool list. * * @param installedCleanupIdeTools the list of installed tools to check against. * @param softwareFolder The software folder of the IDEasy project to scan for used software. * @param projectName The name of the project we are currently scanning. */ private void discoverUsedSoftware(List installedCleanupIdeTools, Path softwareFolder, String projectName) { + discoverUsedSoftware(installedCleanupIdeTools, softwareFolder, projectName, 0); + } + + /** + * This method scans the software folder of an IDEasy project recursively for installed tools and matches these against the global tool list created earlier. + * + * @param installedCleanupIdeTools the list of installed tools to check against. + * @param softwareFolder The software folder of the IDEasy project to scan for used software. + * @param projectName The name of the project we are currently scanning. + * @param depth The current recursion depth. + */ + private void discoverUsedSoftware(List installedCleanupIdeTools, Path softwareFolder, String projectName, int depth) { // Get all installed tools for this project List subfolders = this.context.getFileAccess().listChildren(softwareFolder, Files::isDirectory); for (Path currentFolder : subfolders) { // Converts the path of the tool installation to the real path by eliminating symlinks. This allows us to determine whether a tool is installed locally for an IDEasy project or is part - // of the global software installation under $IDE_ROOT/_ide/software/default - currentFolder = this.context.getFileAccess().toRealPath(currentFolder); - // Check if directory contains a software version file. If so, read version and add to list of used software. - if (Files.exists(currentFolder.resolve(IdeContext.FILE_SOFTWARE_VERSION)) - || Files.exists(currentFolder.resolve(IdeContext.FILE_LEGACY_SOFTWARE_VERSION))) { - if (!currentFolder.startsWith(this.context.getSoftwareRepositoryPath().resolve(ToolRepository.ID_DEFAULT))) { - // We found a software that is locally installed in an IDEasy project but not in the global software folder. We leave these alone. - continue; - } - // Get details of the tool (name, edition, version) - // currentFolder has the structure «repo-path»/«tool»/«edition»/«version» - String toolVersion = currentFolder.getFileName().toString(); - Path toolEditionFolder = currentFolder.getParent(); - String toolEdition = toolEditionFolder.getFileName().toString(); - String toolName = toolEditionFolder.getParent().getFileName().toString(); - // Check if software exists in global CleanupIdeTool list. If so, mark as used - for (CleanupIdeTool tool : installedCleanupIdeTools) { - if (tool.toolName.equals(toolName)) { - tool.getUsedBy().add(projectName); - for (CleanupIdeToolEdition edition : tool.getEditions()) { - if (edition.editionName.equals(toolEdition)) { - edition.getUsedBy().add(projectName); - for (CleanupIdeToolEditionVersion version : edition.getVersions()) { - if (version.versionName.equals(toolVersion)) { - version.getUsedBy().add(projectName); - break; - } - } - break; - } - } - break; + // of the global software installation under $IDE_ROOT/_ide/software + Path referencedPath = this.context.getFileAccess().toRealPath(currentFolder); + // Check if the resolved project software path belongs to a discovered global software version. + boolean matchingVersionFound = markMatchingVersionAsUsed(installedCleanupIdeTools, referencedPath, projectName); + // For ide-extra-tools.json the actual software link may be located at software/extra//. + if (!matchingVersionFound && depth < 1 && !Files.isSymbolicLink(currentFolder)) { + discoverUsedSoftware(installedCleanupIdeTools, currentFolder, projectName, depth + 1); + } + } + } + + /** + * Marks the installed version containing the referenced project software path as used. The referenced path may point directly to a version folder or to one + * of its subfolders. + * + * @param installedCleanupIdeTools the list of installed tools to check against. + * @param referencedPath The resolved path referenced by the IDEasy project. + * @param projectName The name of the project we are currently scanning. + * @return {@code true} if a matching installed version was found. + */ + private boolean markMatchingVersionAsUsed(List installedCleanupIdeTools, Path referencedPath, String projectName) { + for (CleanupIdeTool tool : installedCleanupIdeTools) { + for (CleanupIdeToolEdition edition : tool.getEditions()) { + for (CleanupIdeToolEditionVersion version : edition.getVersions()) { + Path versionPath = version.getPath(); + if (referencedPath.equals(versionPath) || referencedPath.startsWith(versionPath)) { + version.addUsedBy(projectName); + return true; } } } } + return false; } /** @@ -201,8 +217,8 @@ private void markUnusedSoftwareForDeletion(List installedCleanup } /** - * Generates a summary report for tools, editions, and versions to be deleted and prompts the user for confirmation. - * If the user agrees, we proceed with deletion of the unused tools, editions, and versions. + * Generates a summary report for tools, editions, and versions to be deleted and prompts the user for confirmation. If the user agrees, we proceed with + * deletion of the unused tools, editions, and versions. * * @param installedCleanupIdeTools the list of installed tools with deletion flags set. */ @@ -248,17 +264,16 @@ private void logSoftwareToBeDeleted(List installedCleanupIdeTool LOG.info("No installed tools will be deleted. All installed software is used by at least one project."); } else { LOG.info("The following installed tools will be deleted: \n" + logOutput); - LOG.info("Summary: {} installed tool versions across {} editions of {} tools will be deleted.", totalVersionsDeleted, totalEditionsDeleted, totalToolsDeleted); + LOG.info("Summary: {} installed tool versions across {} editions of {} tools will be deleted.", totalVersionsDeleted, totalEditionsDeleted, + totalToolsDeleted); - // Ask for confirmation. Skipped if --force-delete flag is provided - if (!this.forceDelete.isTrue()) { - try { - this.context.askToContinue("Do you want to continue?"); + // Ask for confirmation. Automatically confirmed if the global force option is provided. + try { + this.context.askToContinue("Do you want to continue?"); - } catch (CliAbortException e) { - LOG.info("Installed Tools will not be deleted."); - return; - } + } catch (CliAbortException e) { + LOG.info("Installed Tools will not be deleted."); + return; } deleteUnusedSoftware(installedCleanupIdeTools); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java index d7e55cd216..fd5add8235 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java @@ -7,8 +7,7 @@ /** * Represents an installed IDE tool in the global software folder as discovered by the {@code cleanup} commandlet. *

- * Contains the tool name, installation path, a list of projects that use this tool, - * and a flag indicating whether the tool is marked for deletion. This class also + * Contains the tool name, installation path, and a flag indicating whether the tool is marked for deletion. This class also * holds a list of {@link CleanupIdeToolEdition editions} belonging to this tool. */ public class CleanupIdeTool { @@ -19,9 +18,6 @@ public class CleanupIdeTool { /** The installation {@link Path} of this tool. */ private final Path path; - /** A list of project names that currently use this tool. */ - private final List usedBy; - /** A flag indicating whether the tool is marked for deletion. */ private boolean delete; @@ -38,7 +34,6 @@ public CleanupIdeTool(String toolName, Path path) { this.toolName = toolName; this.path = path; - this.usedBy = new ArrayList<>(); this.delete = false; this.editions = new ArrayList<>(); } @@ -51,24 +46,6 @@ public Path getPath() { return this.path; } - /** - * @return the list of project names that currently use this tool. - */ - public List getUsedBy() { - - return this.usedBy; - } - - /** - * Marks this tool as used by the given project. - * - * @param projectName the name of the project that uses this tool. - */ - public void markUsedBy(String projectName) { - - this.usedBy.add(projectName); - } - /** * @return {@code true} if this tool is marked for deletion. */ @@ -88,11 +65,11 @@ public void setDelete(boolean delete) { } /** - * @return {@code true} if no project currently uses this tool. + * @return {@code true} if all editions of this tool are unused. */ public boolean isUnused() { - return this.usedBy.isEmpty(); + return this.editions.stream().allMatch(CleanupIdeToolEdition::isUnused); } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java index 06e79a03b3..ac01db03a9 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java @@ -8,8 +8,8 @@ * Represents an edition of an IDE tool in the global software folder as discovered by the {@code cleanup} commandlet. *

* For example, for IntelliJ, the editions could be "community" and "ultimate". This class - * contains the edition name, the installation path, a list of projects that use this - * edition, and a flag indicating whether the edition is marked for deletion. It also holds + * contains the edition name, the installation path, and a flag indicating whether the + * edition is marked for deletion. It also holds * a list of {@link CleanupIdeToolEditionVersion versions} belonging to this edition. */ public class CleanupIdeToolEdition { @@ -20,9 +20,6 @@ public class CleanupIdeToolEdition { /** The installation {@link Path} of this edition. */ private final Path path; - /** A list of project names that currently use this edition. */ - private final List usedBy; - /** A flag indicating whether the edition is marked for deletion. */ private boolean delete; @@ -39,7 +36,6 @@ public CleanupIdeToolEdition(String editionName, Path path) { this.editionName = editionName; this.path = path; - this.usedBy = new ArrayList<>(); this.delete = false; this.versions = new ArrayList<>(); } @@ -52,24 +48,6 @@ public Path getPath() { return this.path; } - /** - * @return the list of project names that currently use this edition. - */ - public List getUsedBy() { - - return this.usedBy; - } - - /** - * Marks this edition as used by the given project. - * - * @param projectName the name of the project using this edition. - */ - public void addUsedBy(String projectName) { - - this.usedBy.add(projectName); - } - /** * @return {@code true} if this edition is marked for deletion. */ @@ -89,11 +67,11 @@ public void setDelete(boolean delete) { } /** - * @return {@code true} if no project currently uses this edition. + * @return {@code true} if all versions of this edition are unused. */ public boolean isUnused() { - return this.usedBy.isEmpty(); + return this.versions.stream().allMatch(CleanupIdeToolEditionVersion::isUnused); } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEditionVersion.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEditionVersion.java index e4ceada3ad..ef84cc36af 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEditionVersion.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEditionVersion.java @@ -62,7 +62,9 @@ public List getUsedBy() { */ public void addUsedBy(String projectName) { - this.usedBy.add(projectName); + if (!this.usedBy.contains(projectName)) { + this.usedBy.add(projectName); + } } /** diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 2c4315a3d8..7ab93dedec 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -62,7 +62,7 @@ cmd.inso=Tool commandlet for Inso CLI (Insomnia). cmd.inso.detail=Inso CLI is a command line tool that allows you to use Insomnia features in your terminal and CI/CD environments for automation. Detailed documentation can be found at https://docs.insomnia.rest/inso-cli/introduction cmd.install=Install the selected tool. cmd.install-plugin=Install the selected plugin for the selected tool. -cmd.install-plugin.detail=Plugins can be installed for tools that support such. Plugins must be defined in the project settings to be available to the user for installation. If a desired plugin is not available to install, please contact your ide-admin. +cmd.install-plugin.detail=Plugins can be installed for tools that support such. Plugins must be defined in the project settings to be available to the user for installation. If a desired plugin is not available to install, please contact your ide-admin. cmd.install.detail=To see all tools available for installation, call "ide help" cmd.intellij=Tool commandlet for IntelliJ (IDE) cmd.intellij.detail=IntelliJ is a popular Integrated Development Environment for Java developed by JetBrains. Detailed documentation can be found at https://www.jetbrains.com/idea/documentation/ @@ -179,7 +179,6 @@ opt.--batch=enable batch mode (non-interactive). opt.--code=clone given code repository containing a settings folder into workspaces so that settings can be committed alongside code changes. opt.--debug=enable debug logging. opt.--force=enable force mode. -opt.--force-delete=Delete tools without confirmation opt.--force-plugin-reinstall=resets installed plugins to the project configuration opt.--force-plugins=force plugin (re)installation. opt.--force-pull=force pull of settings even for code repository. diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index 1059d2c4fb..f03cd2cd34 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -179,7 +179,6 @@ opt.--batch=Aktiviert den Batch-Modus (nicht-interaktive Stapelverarbeitung). opt.--code=Git-Repository sowohl als Code- als auch als Settings-Repository verwenden. opt.--debug=Aktiviert Debug-Ausgaben (Fehleranalyse). opt.--force=Aktiviert den Force-Modus (Erzwingen). -opt.--force-delete=Ungenutzte Werkzeuge ohne Bestätigung löschen. opt.--force-plugin-reinstall=Setzt installierte Plugins zurück auf die Projektkonfiguration. opt.--force-plugins=Erzwingt die (Re)Installation von Plugins. opt.--force-pull=Erzwingt einen Pull der settings auch im Fall eines Code-Repositories. diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java index ae2373ade8..8ed6c07e69 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java @@ -1,9 +1,14 @@ package com.devonfw.tools.ide.commandlet; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + import org.junit.jupiter.api.Test; import com.devonfw.tools.ide.commandlet.cleanup.CleanupCommandlet; import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeTestContext; /** @@ -14,21 +19,183 @@ class CleanupCommandletTest extends AbstractIdeContextTest { private static final String PROJECT_BASIC = "basic"; /** - * Test of {@link CleanupCommandlet} that {@code az} tool is not used by any project and thus deleted. + * Tests that unused software is deleted while software used by a project is retained. + * + * @throws IOException if the test setup cannot be created. */ @Test - void testCleanupDeletesUnusedGlobalSoftware() { + void testCleanupDeletesUnusedAndKeepsUsedGlobalSoftware() throws IOException { // arrange IdeTestContext context = newContext(PROJECT_BASIC); - CleanupCommandlet cleanup = context.getCommandletManager().getCommandlet(CleanupCommandlet.class); - cleanup.forceDelete.setValue(true); + + Path usedVersion = createInstalledVersion(context, "default", "cleanup-test-java", "default", "21"); + + Path unusedVersion = createInstalledVersion(context, "default", "cleanup-test-java", "default", "17"); + + Path projectSoftware = context.getIdeHome().resolve(IdeContext.FOLDER_SOFTWARE); + + createSoftwareLink(projectSoftware.resolve("cleanup-test-java"), usedVersion); + + CleanupCommandlet cleanup = getCleanupWithoutConfirmation(context); // act cleanup.run(); // assert - assertThat(context.getIdeRoot().resolve("_ide/software/default/az")).doesNotExist(); + assertThat(usedVersion).as("Software used by a project must not be deleted").exists(); + + assertThat(unusedVersion).as("Unused software should be deleted").doesNotExist(); + assertThat(context).logAtSuccess().hasMessage("Unused tools have been deleted successfully."); } + + /** + * Tests that a version is retained when a project links to a subdirectory of the installation, for example {@code /Contents/MacOS}. + * + * @throws IOException if the test setup cannot be created. + */ + @Test + void testCleanupKeepsVersionReferencedBySubdirectory() throws IOException { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + Path usedVersion = createInstalledVersion(context, "default", "cleanup-test-macos", "default", "1.0"); + + Path macOsFolder = usedVersion.resolve("Contents").resolve("MacOS"); + Files.createDirectories(macOsFolder); + + Path unusedVersion = createInstalledVersion(context, "default", "cleanup-test-macos", "default", "2.0"); + + Path projectSoftware = context.getIdeHome().resolve(IdeContext.FOLDER_SOFTWARE); + + createSoftwareLink(projectSoftware.resolve("cleanup-test-macos"), macOsFolder); + + CleanupCommandlet cleanup = getCleanupWithoutConfirmation(context); + + // act + cleanup.run(); + + // assert + assertThat(usedVersion) + .as("The version must be retained when the project links to a subdirectory of that version") + .exists(); + + assertThat(unusedVersion).as("The unrelated unused version should be deleted").doesNotExist(); + } + + /** + * Tests that a version referenced by {@code software/extra//} is retained. + * + * @throws IOException if the test setup cannot be created. + */ + @Test + void testCleanupKeepsVersionReferencedByNestedExtraTool() throws IOException { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + Path usedVersion = createInstalledVersion(context, "default", "cleanup-test-extra", "default", "1.0"); + + Path unusedVersion = createInstalledVersion(context, "default", "cleanup-test-extra", "default", "2.0"); + + Path nestedExtraToolLink = context.getIdeHome() + .resolve(IdeContext.FOLDER_SOFTWARE) + .resolve(IdeContext.FOLDER_EXTRA) + .resolve("cleanup-test-extra") + .resolve("custom-installation"); + + createSoftwareLink(nestedExtraToolLink, usedVersion); + + CleanupCommandlet cleanup = getCleanupWithoutConfirmation(context); + + // act + cleanup.run(); + + // assert + assertThat(usedVersion) + .as("Software referenced by software/extra// must not be deleted") + .exists(); + + assertThat(unusedVersion).as("The unrelated unused extra-tool version should be deleted").doesNotExist(); + } + + /** + * Tests that unused installations in repositories other than {@code default} are also discovered and deleted. + * + * @throws IOException if the test setup cannot be created. + */ + @Test + void testCleanupDeletesUnusedSoftwareFromNonDefaultRepository() throws IOException { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + Path unusedVersion = createInstalledVersion(context, "cleanup-test-repository", "cleanup-test-tool", "default", + "1.0"); + + CleanupCommandlet cleanup = getCleanupWithoutConfirmation(context); + + // act + cleanup.run(); + + // assert + assertThat(unusedVersion) + .as("Unused software from a non-default repository should also be discovered and deleted") + .doesNotExist(); + } + + /** + * Creates an installed software version with the structure {@code _ide/software////}. + * + * @param context the test context. + * @param repositoryId the repository ID. + * @param toolName the tool name. + * @param editionName the edition name. + * @param versionName the version name. + * @return the real path of the created version directory. + * @throws IOException if the directory or version marker cannot be created. + */ + private Path createInstalledVersion(IdeTestContext context, String repositoryId, String toolName, + String editionName, String versionName) throws IOException { + + Path versionFolder = context.getSoftwareRepositoryPath() + .resolve(repositoryId) + .resolve(toolName) + .resolve(editionName) + .resolve(versionName); + + Files.createDirectories(versionFolder); + + Files.writeString(versionFolder.resolve(IdeContext.FILE_SOFTWARE_VERSION), versionName); + + return versionFolder.toRealPath(); + } + + /** + * Creates a symbolic link from a project software location to an installed software version or one of its subdirectories. + * + * @param link the project-side link. + * @param target the link target. + * @throws IOException if the link cannot be created. + */ + private void createSoftwareLink(Path link, Path target) throws IOException { + + Files.createDirectories(link.getParent()); + Files.deleteIfExists(link); + Files.createSymbolicLink(link, target); + } + + /** + * Gets the cleanup commandlet and configures the confirmation answer. + * + * @param context the test context. + * @return the configured cleanup commandlet. + */ + private CleanupCommandlet getCleanupWithoutConfirmation(IdeTestContext context) { + + context.setAnswers("yes"); + return context.getCommandletManager().getCommandlet(CleanupCommandlet.class); + } } From d7b4b02c15b71ae327b873f56e7ba3b74bc98223 Mon Sep 17 00:00:00 2001 From: caylipp Date: Wed, 29 Jul 2026 13:33:06 +0200 Subject: [PATCH 21/27] #1953: fix comment length --- .../tools/ide/commandlet/cleanup/CleanupCommandlet.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java index cbf74b1209..4134b177cd 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java @@ -157,7 +157,8 @@ private void discoverUsedSoftware(List installedCleanupIdeTools, // Get all installed tools for this project List subfolders = this.context.getFileAccess().listChildren(softwareFolder, Files::isDirectory); for (Path currentFolder : subfolders) { - // Converts the path of the tool installation to the real path by eliminating symlinks. This allows us to determine whether a tool is installed locally for an IDEasy project or is part + // Converts the path of the tool installation to the real path by eliminating symlinks. This allows us to determine + // whether a tool is installed locally for an IDEasy project or is part // of the global software installation under $IDE_ROOT/_ide/software Path referencedPath = this.context.getFileAccess().toRealPath(currentFolder); // Check if the resolved project software path belongs to a discovered global software version. From 3665d3e36a80b3d0cf514bc20dfa44eb276afc59 Mon Sep 17 00:00:00 2001 From: caylipp Date: Thu, 30 Jul 2026 14:02:15 +0200 Subject: [PATCH 22/27] #1953: Introduce installed software hierarchy - add common abstract base class for installed software items - centralize software item name and path - rename cleanup model classes for reuse --- .../AbstractInstalledSoftwareItem.java | 43 +++++++ .../commandlet/cleanup/CleanupCommandlet.java | 107 +++++++++--------- .../commandlet/cleanup/CleanupIdeTool.java | 88 -------------- .../cleanup/CleanupIdeToolEdition.java | 90 --------------- .../cleanup/InstalledSoftwareEdition.java | 73 ++++++++++++ .../cleanup/InstalledSoftwareTool.java | 73 ++++++++++++ ...ion.java => InstalledSoftwareVersion.java} | 30 ++--- 7 files changed, 252 insertions(+), 252 deletions(-) create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/AbstractInstalledSoftwareItem.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java delete mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareEdition.java create mode 100644 cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareTool.java rename cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/{CleanupIdeToolEditionVersion.java => InstalledSoftwareVersion.java} (68%) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/AbstractInstalledSoftwareItem.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/AbstractInstalledSoftwareItem.java new file mode 100644 index 0000000000..31ece0b728 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/AbstractInstalledSoftwareItem.java @@ -0,0 +1,43 @@ +package com.devonfw.tools.ide.commandlet.cleanup; + +import java.nio.file.Path; + +/** + * Abstract base class for an item in the installed software hierarchy. + */ +public abstract class AbstractInstalledSoftwareItem { + + /** The name of this item. */ + private final String name; + + /** The installation path of this item. */ + private final Path path; + + /** + * Constructor. + * + * @param name the name of this item. + * @param path the installation path of this item. + */ + protected AbstractInstalledSoftwareItem(String name, Path path) { + + this.name = name; + this.path = path; + } + + /** + * @return the name of this item. + */ + public String getName() { + + return this.name; + } + + /** + * @return the installation path of this item. + */ + public Path getPath() { + + return this.path; + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java index 4134b177cd..3384aaf4d5 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java @@ -21,6 +21,11 @@ public class CleanupCommandlet extends Commandlet { private static final Logger LOG = LoggerFactory.getLogger(CleanupCommandlet.class); + /** + * Constructor. + * + * @param context the {@link IdeContext}. + */ public CleanupCommandlet(IdeContext context) { super(context); @@ -50,7 +55,7 @@ protected void doRun() { */ private void discoverAndDeleteUnusedSoftware() { // Iterate over software in $IDE_ROOT/_ide/software repositories and save installed software to a list - List installedCleanupIdeTools = discoverInstalledSoftware(); + List installedSoftwareTools = discoverInstalledSoftware(); // Scan for IDEasy projects List ideasyProjects = this.context.getFileAccess().listChildren(this.context.getIdeRoot(), Files::isDirectory); @@ -61,14 +66,14 @@ private void discoverAndDeleteUnusedSoftware() { continue; } Path ideasyProjectSoftware = ideasyProject.resolve(IdeContext.FOLDER_SOFTWARE); - discoverUsedSoftware(installedCleanupIdeTools, ideasyProjectSoftware, ideasyProject.getFileName().toString()); - discoverUsedSoftware(installedCleanupIdeTools, ideasyProjectSoftware.resolve(IdeContext.FOLDER_EXTRA), ideasyProject.getFileName().toString()); + discoverUsedSoftware(installedSoftwareTools, ideasyProjectSoftware, ideasyProject.getFileName().toString()); + discoverUsedSoftware(installedSoftwareTools, ideasyProjectSoftware.resolve(IdeContext.FOLDER_EXTRA), ideasyProject.getFileName().toString()); } // Mark unused software for deletion - markUnusedSoftwareForDeletion(installedCleanupIdeTools); + markUnusedSoftwareForDeletion(installedSoftwareTools); // Log summary report and proceed with deletion if user confirms - logSoftwareToBeDeleted(installedCleanupIdeTools); + logSoftwareToBeDeleted(installedSoftwareTools); } /** @@ -76,27 +81,27 @@ private void discoverAndDeleteUnusedSoftware() { * * @return the list of discovered installed tools. */ - private List discoverInstalledSoftware() { - List installedCleanupIdeTools = new ArrayList<>(); + private List discoverInstalledSoftware() { + List installedSoftwareTools = new ArrayList<>(); Path softwareRepositoryPath = this.context.getSoftwareRepositoryPath(); List repositoryFolders = this.context.getFileAccess().listChildren(softwareRepositoryPath, Files::isDirectory); for (Path repositoryFolder : repositoryFolders) { - discoverInstalledSoftwareRepository(installedCleanupIdeTools, repositoryFolder); + discoverInstalledSoftwareRepository(installedSoftwareTools, repositoryFolder); } - return installedCleanupIdeTools; + return installedSoftwareTools; } /** * This method discovers all installed tools in one software repository. Installed editions are then recursively discovered. * - * @param installedCleanupIdeTools the list to populate with discovered tools. + * @param installedSoftwareTools the list to populate with discovered tools. * @param repositoryFolder The software repository folder to scan. */ - private void discoverInstalledSoftwareRepository(List installedCleanupIdeTools, Path repositoryFolder) { + private void discoverInstalledSoftwareRepository(List installedSoftwareTools, Path repositoryFolder) { List toolFolders = this.context.getFileAccess().listChildren(repositoryFolder, Files::isDirectory); for (Path toolFolder : toolFolders) { - CleanupIdeTool tool = new CleanupIdeTool(toolFolder.getFileName().toString(), this.context.getFileAccess().toRealPath(toolFolder)); - installedCleanupIdeTools.add(tool); + InstalledSoftwareTool tool = new InstalledSoftwareTool(toolFolder.getFileName().toString(), this.context.getFileAccess().toRealPath(toolFolder)); + installedSoftwareTools.add(tool); discoverInstalledEditions(toolFolder, tool); } } @@ -108,10 +113,10 @@ private void discoverInstalledSoftwareRepository(List installedC * @param editionFolder The folder where the editions are saved in ($IDE_ROOT/_ide/software//). * @param tool The respective tool for which we are discovering editions. */ - private void discoverInstalledEditions(Path editionFolder, CleanupIdeTool tool) { + private void discoverInstalledEditions(Path editionFolder, InstalledSoftwareTool tool) { List subfolders = this.context.getFileAccess().listChildren(editionFolder, Files::isDirectory); for (Path subfolder : subfolders) { - CleanupIdeToolEdition edition = new CleanupIdeToolEdition(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); + InstalledSoftwareEdition edition = new InstalledSoftwareEdition(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); tool.getEditions().add(edition); discoverInstalledVersions(subfolder, edition); } @@ -124,10 +129,10 @@ private void discoverInstalledEditions(Path editionFolder, CleanupIdeTool tool) * @param versionFolder The folder where the versions are saved in ($IDE_ROOT/_ide/software///). * @param edition The respective edition for which we are discovering versions. */ - private void discoverInstalledVersions(Path versionFolder, CleanupIdeToolEdition edition) { + private void discoverInstalledVersions(Path versionFolder, InstalledSoftwareEdition edition) { List subfolders = this.context.getFileAccess().listChildren(versionFolder, Files::isDirectory); for (Path subfolder : subfolders) { - CleanupIdeToolEditionVersion version = new CleanupIdeToolEditionVersion(subfolder.getFileName().toString(), + InstalledSoftwareVersion version = new InstalledSoftwareVersion(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); edition.getVersions().add(version); } @@ -137,23 +142,23 @@ private void discoverInstalledVersions(Path versionFolder, CleanupIdeToolEdition * This method scans the software folder of an IDEasy project for installed tools and matches these against the global tool list created earlier. Identified * tools are marked as used in the global tool list. * - * @param installedCleanupIdeTools the list of installed tools to check against. + * @param installedSoftwareTools the list of installed tools to check against. * @param softwareFolder The software folder of the IDEasy project to scan for used software. * @param projectName The name of the project we are currently scanning. */ - private void discoverUsedSoftware(List installedCleanupIdeTools, Path softwareFolder, String projectName) { - discoverUsedSoftware(installedCleanupIdeTools, softwareFolder, projectName, 0); + private void discoverUsedSoftware(List installedSoftwareTools, Path softwareFolder, String projectName) { + discoverUsedSoftware(installedSoftwareTools, softwareFolder, projectName, 0); } /** * This method scans the software folder of an IDEasy project recursively for installed tools and matches these against the global tool list created earlier. * - * @param installedCleanupIdeTools the list of installed tools to check against. + * @param installedSoftwareTools the list of installed tools to check against. * @param softwareFolder The software folder of the IDEasy project to scan for used software. * @param projectName The name of the project we are currently scanning. * @param depth The current recursion depth. */ - private void discoverUsedSoftware(List installedCleanupIdeTools, Path softwareFolder, String projectName, int depth) { + private void discoverUsedSoftware(List installedSoftwareTools, Path softwareFolder, String projectName, int depth) { // Get all installed tools for this project List subfolders = this.context.getFileAccess().listChildren(softwareFolder, Files::isDirectory); for (Path currentFolder : subfolders) { @@ -162,10 +167,10 @@ private void discoverUsedSoftware(List installedCleanupIdeTools, // of the global software installation under $IDE_ROOT/_ide/software Path referencedPath = this.context.getFileAccess().toRealPath(currentFolder); // Check if the resolved project software path belongs to a discovered global software version. - boolean matchingVersionFound = markMatchingVersionAsUsed(installedCleanupIdeTools, referencedPath, projectName); + boolean matchingVersionFound = markMatchingVersionAsUsed(installedSoftwareTools, referencedPath, projectName); // For ide-extra-tools.json the actual software link may be located at software/extra//. if (!matchingVersionFound && depth < 1 && !Files.isSymbolicLink(currentFolder)) { - discoverUsedSoftware(installedCleanupIdeTools, currentFolder, projectName, depth + 1); + discoverUsedSoftware(installedSoftwareTools, currentFolder, projectName, depth + 1); } } } @@ -174,15 +179,15 @@ private void discoverUsedSoftware(List installedCleanupIdeTools, * Marks the installed version containing the referenced project software path as used. The referenced path may point directly to a version folder or to one * of its subfolders. * - * @param installedCleanupIdeTools the list of installed tools to check against. + * @param installedSoftwareTools the list of installed tools to check against. * @param referencedPath The resolved path referenced by the IDEasy project. * @param projectName The name of the project we are currently scanning. * @return {@code true} if a matching installed version was found. */ - private boolean markMatchingVersionAsUsed(List installedCleanupIdeTools, Path referencedPath, String projectName) { - for (CleanupIdeTool tool : installedCleanupIdeTools) { - for (CleanupIdeToolEdition edition : tool.getEditions()) { - for (CleanupIdeToolEditionVersion version : edition.getVersions()) { + private boolean markMatchingVersionAsUsed(List installedSoftwareTools, Path referencedPath, String projectName) { + for (InstalledSoftwareTool tool : installedSoftwareTools) { + for (InstalledSoftwareEdition edition : tool.getEditions()) { + for (InstalledSoftwareVersion version : edition.getVersions()) { Path versionPath = version.getPath(); if (referencedPath.equals(versionPath) || referencedPath.startsWith(versionPath)) { version.addUsedBy(projectName); @@ -197,12 +202,12 @@ private boolean markMatchingVersionAsUsed(List installedCleanupI /** * Sets the delete flag for all unused tools, editions, and versions to true. * - * @param installedCleanupIdeTools the list of installed tools to mark. + * @param installedSoftwareTools the list of installed tools to mark. */ - private void markUnusedSoftwareForDeletion(List installedCleanupIdeTools) { - for (CleanupIdeTool tool : installedCleanupIdeTools) { - for (CleanupIdeToolEdition edition : tool.getEditions()) { - for (CleanupIdeToolEditionVersion version : edition.getVersions()) { + private void markUnusedSoftwareForDeletion(List installedSoftwareTools) { + for (InstalledSoftwareTool tool : installedSoftwareTools) { + for (InstalledSoftwareEdition edition : tool.getEditions()) { + for (InstalledSoftwareVersion version : edition.getVersions()) { if (version.isUnused()) { version.setDelete(true); } @@ -221,22 +226,22 @@ private void markUnusedSoftwareForDeletion(List installedCleanup * Generates a summary report for tools, editions, and versions to be deleted and prompts the user for confirmation. If the user agrees, we proceed with * deletion of the unused tools, editions, and versions. * - * @param installedCleanupIdeTools the list of installed tools with deletion flags set. + * @param installedSoftwareTools the list of installed tools with deletion flags set. */ - private void logSoftwareToBeDeleted(List installedCleanupIdeTools) { + private void logSoftwareToBeDeleted(List installedSoftwareTools) { String logOutput = ""; int totalToolsDeleted = 0; int totalEditionsDeleted = 0; int totalVersionsDeleted = 0; - for (CleanupIdeTool tool : installedCleanupIdeTools) { + for (InstalledSoftwareTool tool : installedSoftwareTools) { String logOutputEdition = ""; int editionsDeleted = 0; - for (CleanupIdeToolEdition edition : tool.getEditions()) { + for (InstalledSoftwareEdition edition : tool.getEditions()) { String logOutputVersion = ""; int versionsDeleted = 0; - for (CleanupIdeToolEditionVersion version : edition.getVersions()) { + for (InstalledSoftwareVersion version : edition.getVersions()) { if (version.isDelete()) { - logOutputVersion += "\t\t - " + version.versionName + "\n"; + logOutputVersion += "\t\t - " + version.getName() + "\n"; versionsDeleted++; totalVersionsDeleted++; } @@ -246,7 +251,7 @@ private void logSoftwareToBeDeleted(List installedCleanupIdeTool if (versionsDeleted < edition.getVersions().size()) { logOutputVersion += "\t\t + " + (edition.getVersions().size() - versionsDeleted) + " more version(s) of this edition will not be deleted\n"; } - logOutputEdition += "\t - " + edition.editionName + "\n" + logOutputVersion; + logOutputEdition += "\t - " + edition.getName() + "\n" + logOutputVersion; editionsDeleted++; totalEditionsDeleted++; } @@ -256,7 +261,7 @@ private void logSoftwareToBeDeleted(List installedCleanupIdeTool if (editionsDeleted < tool.getEditions().size()) { logOutputEdition += "\t + " + (tool.getEditions().size() - editionsDeleted) + " more edition(s) of this tool will not be deleted\n"; } - logOutput += " - " + tool.toolName + "\n" + logOutputEdition; + logOutput += " - " + tool.getName() + "\n" + logOutputEdition; totalToolsDeleted++; } } @@ -276,35 +281,35 @@ private void logSoftwareToBeDeleted(List installedCleanupIdeTool LOG.info("Installed Tools will not be deleted."); return; } - deleteUnusedSoftware(installedCleanupIdeTools); + deleteUnusedSoftware(installedSoftwareTools); } } /** * Deletes tools, editions, and versions marked for deletion. * - * @param installedCleanupIdeTools the list of installed tools to delete from. + * @param installedSoftwareTools the list of installed tools to delete from. */ - private void deleteUnusedSoftware(List installedCleanupIdeTools) { + private void deleteUnusedSoftware(List installedSoftwareTools) { int failedDeletion = 0; // Delete the tool - for (CleanupIdeTool tool : installedCleanupIdeTools) { + for (InstalledSoftwareTool tool : installedSoftwareTools) { if (tool.isDelete()) { - LOG.debug("Deleting tool {} and all its editions and versions in {}", tool.toolName, tool.getPath()); + LOG.debug("Deleting tool {} and all its editions and versions in {}", tool.getName(), tool.getPath()); failedDeletion += deleteFolder(tool.getPath()); continue; } // Delete editions of the tool - for (CleanupIdeToolEdition edition : tool.getEditions()) { + for (InstalledSoftwareEdition edition : tool.getEditions()) { if (edition.isDelete()) { - LOG.debug("Deleting edition {} of tool {} and all its versions in {}", edition.editionName, tool.toolName, edition.getPath()); + LOG.debug("Deleting edition {} of tool {} and all its versions in {}", edition.getName(), tool.getName(), edition.getPath()); failedDeletion += deleteFolder(edition.getPath()); continue; } // Delete versions of the edition - for (CleanupIdeToolEditionVersion version : edition.getVersions()) { + for (InstalledSoftwareVersion version : edition.getVersions()) { if (version.isDelete()) { - LOG.debug("Deleting version {} of edition {} of tool {} in {}", version.versionName, edition.editionName, tool.toolName, version.getPath()); + LOG.debug("Deleting version {} of edition {} of tool {} in {}", version.getName(), edition.getName(), tool.getName(), version.getPath()); failedDeletion += deleteFolder(version.getPath()); } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java deleted file mode 100644 index fd5add8235..0000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeTool.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.devonfw.tools.ide.commandlet.cleanup; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -/** - * Represents an installed IDE tool in the global software folder as discovered by the {@code cleanup} commandlet. - *

- * Contains the tool name, installation path, and a flag indicating whether the tool is marked for deletion. This class also - * holds a list of {@link CleanupIdeToolEdition editions} belonging to this tool. - */ -public class CleanupIdeTool { - - /** The name of this tool. */ - public final String toolName; - - /** The installation {@link Path} of this tool. */ - private final Path path; - - /** A flag indicating whether the tool is marked for deletion. */ - private boolean delete; - - /** A list of {@link CleanupIdeToolEdition editions} belonging to this tool. */ - private final List editions; - - /** - * Constructor. - * - * @param toolName the name of the tool. - * @param path the installation {@link Path} of this tool. - */ - public CleanupIdeTool(String toolName, Path path) { - - this.toolName = toolName; - this.path = path; - this.delete = false; - this.editions = new ArrayList<>(); - } - - /** - * @return the installation {@link Path} of this tool. - */ - public Path getPath() { - - return this.path; - } - - /** - * @return {@code true} if this tool is marked for deletion. - */ - public boolean isDelete() { - - return this.delete; - } - - /** - * Sets the deletion flag. - * - * @param delete {@code true} to mark this tool for deletion. - */ - public void setDelete(boolean delete) { - - this.delete = delete; - } - - /** - * @return {@code true} if all editions of this tool are unused. - */ - public boolean isUnused() { - - return this.editions.stream().allMatch(CleanupIdeToolEdition::isUnused); - } - - /** - * @return the list of {@link CleanupIdeToolEdition editions} belonging to this tool. - */ - public List getEditions() { - - return this.editions; - } - - @Override - public String toString() { - - return "CleanupIdeTool[" + this.toolName + "]"; - } -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java deleted file mode 100644 index ac01db03a9..0000000000 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEdition.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.devonfw.tools.ide.commandlet.cleanup; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -/** - * Represents an edition of an IDE tool in the global software folder as discovered by the {@code cleanup} commandlet. - *

- * For example, for IntelliJ, the editions could be "community" and "ultimate". This class - * contains the edition name, the installation path, and a flag indicating whether the - * edition is marked for deletion. It also holds - * a list of {@link CleanupIdeToolEditionVersion versions} belonging to this edition. - */ -public class CleanupIdeToolEdition { - - /** The name of this edition. */ - public final String editionName; - - /** The installation {@link Path} of this edition. */ - private final Path path; - - /** A flag indicating whether the edition is marked for deletion. */ - private boolean delete; - - /** A list of {@link CleanupIdeToolEditionVersion versions} belonging to this edition. */ - private final List versions; - - /** - * Constructor. - * - * @param editionName the name of the edition. - * @param path the installation {@link Path} of this edition. - */ - public CleanupIdeToolEdition(String editionName, Path path) { - - this.editionName = editionName; - this.path = path; - this.delete = false; - this.versions = new ArrayList<>(); - } - - /** - * @return the installation {@link Path} of this edition. - */ - public Path getPath() { - - return this.path; - } - - /** - * @return {@code true} if this edition is marked for deletion. - */ - public boolean isDelete() { - - return this.delete; - } - - /** - * Sets the deletion flag. - * - * @param delete {@code true} to mark this edition for deletion. - */ - public void setDelete(boolean delete) { - - this.delete = delete; - } - - /** - * @return {@code true} if all versions of this edition are unused. - */ - public boolean isUnused() { - - return this.versions.stream().allMatch(CleanupIdeToolEditionVersion::isUnused); - } - - /** - * @return the list of {@link CleanupIdeToolEditionVersion versions} belonging to this edition. - */ - public List getVersions() { - - return this.versions; - } - - @Override - public String toString() { - - return "CleanupIdeToolEdition[" + this.editionName + "]"; - } -} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareEdition.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareEdition.java new file mode 100644 index 0000000000..ae9b264507 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareEdition.java @@ -0,0 +1,73 @@ +package com.devonfw.tools.ide.commandlet.cleanup; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an edition of an IDE tool in the global software folder as discovered by the {@code cleanup} commandlet. + *

+ * For example, for IntelliJ, the editions could be "community" and "ultimate". This class contains a flag indicating whether the edition is marked for + * deletion. It also holds a list of {@link InstalledSoftwareVersion versions} belonging to this edition. + */ +public class InstalledSoftwareEdition extends AbstractInstalledSoftwareItem { + + /** A flag indicating whether the edition is marked for deletion. */ + private boolean delete; + + /** A list of {@link InstalledSoftwareVersion versions} belonging to this edition. */ + private final List versions; + + /** + * Constructor. + * + * @param name the name of the edition. + * @param path the installation {@link Path} of this edition. + */ + public InstalledSoftwareEdition(String name, Path path) { + + super(name, path); + this.delete = false; + this.versions = new ArrayList<>(); + } + + /** + * @return {@code true} if this edition is marked for deletion. + */ + public boolean isDelete() { + + return this.delete; + } + + /** + * Sets the deletion flag. + * + * @param delete {@code true} to mark this edition for deletion. + */ + public void setDelete(boolean delete) { + + this.delete = delete; + } + + /** + * @return {@code true} if all versions of this edition are unused. + */ + public boolean isUnused() { + + return this.versions.stream().allMatch(InstalledSoftwareVersion::isUnused); + } + + /** + * @return the list of {@link InstalledSoftwareVersion versions} belonging to this edition. + */ + public List getVersions() { + + return this.versions; + } + + @Override + public String toString() { + + return "InstalledSoftwareEdition[" + getName() + "]"; + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareTool.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareTool.java new file mode 100644 index 0000000000..f09bffd792 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareTool.java @@ -0,0 +1,73 @@ +package com.devonfw.tools.ide.commandlet.cleanup; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an installed IDE tool in the global software folder as discovered by the {@code cleanup} commandlet. + *

+ * Contains a flag indicating whether the tool is marked for deletion and a list of + * {@link InstalledSoftwareEdition editions} belonging to this tool. + */ +public class InstalledSoftwareTool extends AbstractInstalledSoftwareItem { + + /** A flag indicating whether the tool is marked for deletion. */ + private boolean delete; + + /** A list of {@link InstalledSoftwareEdition editions} belonging to this tool. */ + private final List editions; + + /** + * Constructor. + * + * @param name the name of the tool. + * @param path the installation {@link Path} of this tool. + */ + public InstalledSoftwareTool(String name, Path path) { + + super(name, path); + this.delete = false; + this.editions = new ArrayList<>(); + } + + /** + * @return {@code true} if this tool is marked for deletion. + */ + public boolean isDelete() { + + return this.delete; + } + + /** + * Sets the deletion flag. + * + * @param delete {@code true} to mark this tool for deletion. + */ + public void setDelete(boolean delete) { + + this.delete = delete; + } + + /** + * @return {@code true} if all editions of this tool are unused. + */ + public boolean isUnused() { + + return this.editions.stream().allMatch(InstalledSoftwareEdition::isUnused); + } + + /** + * @return the list of {@link InstalledSoftwareEdition editions} belonging to this tool. + */ + public List getEditions() { + + return this.editions; + } + + @Override + public String toString() { + + return "InstalledSoftwareTool[" + getName() + "]"; + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEditionVersion.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareVersion.java similarity index 68% rename from cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEditionVersion.java rename to cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareVersion.java index ef84cc36af..6dd812827f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupIdeToolEditionVersion.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareVersion.java @@ -7,17 +7,10 @@ /** * Represents a version of an IDE tool edition in the global software folder as discovered by the {@code cleanup} commandlet. *

- * For example, for IntelliJ's "ultimate" edition, versions could be "2022.3" and "2023.1". - * This class contains the version name, the installation path, a list of projects that - * use this version, and a flag indicating whether the version is marked for deletion. + * For example, for IntelliJ's "ultimate" edition, versions could be "2022.3" and "2023.1". This class contains a list + * of projects that use this version and a flag indicating whether the version is marked for deletion. */ -public class CleanupIdeToolEditionVersion { - - /** The name of this version. */ - public final String versionName; - - /** The installation {@link Path} of this version. */ - private final Path path; +public class InstalledSoftwareVersion extends AbstractInstalledSoftwareItem { /** A list of project names that currently use this version. */ private final List usedBy; @@ -28,25 +21,16 @@ public class CleanupIdeToolEditionVersion { /** * Constructor. * - * @param versionName the name of the version. + * @param name the name of the version. * @param path the installation {@link Path} of this version. */ - public CleanupIdeToolEditionVersion(String versionName, Path path) { + public InstalledSoftwareVersion(String name, Path path) { - this.versionName = versionName; - this.path = path; + super(name, path); this.usedBy = new ArrayList<>(); this.delete = false; } - /** - * @return the installation {@link Path} of this version. - */ - public Path getPath() { - - return this.path; - } - /** * @return the list of project names that currently use this version. */ @@ -96,6 +80,6 @@ public boolean isUnused() { @Override public String toString() { - return "CleanupIdeToolEditionVersion[" + this.versionName + "]"; + return "InstalledSoftwareVersion[" + getName() + "]"; } } From 79c9f9560a3c1c07b2b20c0cc2fd647a46e2c359 Mon Sep 17 00:00:00 2001 From: caylipp Date: Thu, 30 Jul 2026 14:32:57 +0200 Subject: [PATCH 23/27] #1953: Simplify installed software deletion state - keep deletion state only on software versions - delete unused versions before empty parent folders - remove redundant deletion state from tools and editions --- .../commandlet/cleanup/CleanupCommandlet.java | 46 +++++++++---------- .../cleanup/InstalledSoftwareEdition.java | 34 +------------- .../cleanup/InstalledSoftwareTool.java | 33 +------------ 3 files changed, 26 insertions(+), 87 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java index 3384aaf4d5..ede1baa03a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java @@ -200,9 +200,9 @@ private boolean markMatchingVersionAsUsed(List installedS } /** - * Sets the delete flag for all unused tools, editions, and versions to true. + * Sets the delete flag for all unused software versions to {@code true}. * - * @param installedSoftwareTools the list of installed tools to mark. + * @param installedSoftwareTools the list of installed tools containing the versions to mark. */ private void markUnusedSoftwareForDeletion(List installedSoftwareTools) { for (InstalledSoftwareTool tool : installedSoftwareTools) { @@ -212,12 +212,6 @@ private void markUnusedSoftwareForDeletion(List installed version.setDelete(true); } } - if (edition.isUnused()) { - edition.setDelete(true); - } - } - if (tool.isUnused()) { - tool.setDelete(true); } } } @@ -286,33 +280,28 @@ private void logSoftwareToBeDeleted(List installedSoftwar } /** - * Deletes tools, editions, and versions marked for deletion. + * Deletes software versions marked for deletion and removes their parent edition and tool folders if they become empty. * - * @param installedSoftwareTools the list of installed tools to delete from. + * @param installedSoftwareTools the list of installed tools containing the versions to delete. */ private void deleteUnusedSoftware(List installedSoftwareTools) { int failedDeletion = 0; - // Delete the tool for (InstalledSoftwareTool tool : installedSoftwareTools) { - if (tool.isDelete()) { - LOG.debug("Deleting tool {} and all its editions and versions in {}", tool.getName(), tool.getPath()); - failedDeletion += deleteFolder(tool.getPath()); - continue; - } - // Delete editions of the tool for (InstalledSoftwareEdition edition : tool.getEditions()) { - if (edition.isDelete()) { - LOG.debug("Deleting edition {} of tool {} and all its versions in {}", edition.getName(), tool.getName(), edition.getPath()); - failedDeletion += deleteFolder(edition.getPath()); - continue; - } - // Delete versions of the edition for (InstalledSoftwareVersion version : edition.getVersions()) { if (version.isDelete()) { LOG.debug("Deleting version {} of edition {} of tool {} in {}", version.getName(), edition.getName(), tool.getName(), version.getPath()); failedDeletion += deleteFolder(version.getPath()); } } + if (isEmptyFolder(edition.getPath())) { + LOG.debug("Deleting empty edition {} of tool {} in {}", edition.getName(), tool.getName(), edition.getPath()); + failedDeletion += deleteFolder(edition.getPath()); + } + } + if (isEmptyFolder(tool.getPath())) { + LOG.debug("Deleting empty tool {} in {}", tool.getName(), tool.getPath()); + failedDeletion += deleteFolder(tool.getPath()); } } @@ -324,6 +313,17 @@ private void deleteUnusedSoftware(List installedSoftwareT } } + /** + * Checks whether the given folder exists and has no remaining children. + * + * @param folder the folder to check. + * @return {@code true} if the folder exists and is empty. + */ + private boolean isEmptyFolder(Path folder) { + + return Files.isDirectory(folder) && this.context.getFileAccess().listChildren(folder, child -> true).isEmpty(); + } + /** * Deletes a folder at a given path. Logs an error message if unsuccessful. * diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareEdition.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareEdition.java index ae9b264507..1cf6619b94 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareEdition.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareEdition.java @@ -7,14 +7,11 @@ /** * Represents an edition of an IDE tool in the global software folder as discovered by the {@code cleanup} commandlet. *

- * For example, for IntelliJ, the editions could be "community" and "ultimate". This class contains a flag indicating whether the edition is marked for - * deletion. It also holds a list of {@link InstalledSoftwareVersion versions} belonging to this edition. + * For example, for IntelliJ, the editions could be "community" and "ultimate". This class holds a list of + * {@link InstalledSoftwareVersion versions} belonging to this edition. */ public class InstalledSoftwareEdition extends AbstractInstalledSoftwareItem { - /** A flag indicating whether the edition is marked for deletion. */ - private boolean delete; - /** A list of {@link InstalledSoftwareVersion versions} belonging to this edition. */ private final List versions; @@ -27,36 +24,9 @@ public class InstalledSoftwareEdition extends AbstractInstalledSoftwareItem { public InstalledSoftwareEdition(String name, Path path) { super(name, path); - this.delete = false; this.versions = new ArrayList<>(); } - /** - * @return {@code true} if this edition is marked for deletion. - */ - public boolean isDelete() { - - return this.delete; - } - - /** - * Sets the deletion flag. - * - * @param delete {@code true} to mark this edition for deletion. - */ - public void setDelete(boolean delete) { - - this.delete = delete; - } - - /** - * @return {@code true} if all versions of this edition are unused. - */ - public boolean isUnused() { - - return this.versions.stream().allMatch(InstalledSoftwareVersion::isUnused); - } - /** * @return the list of {@link InstalledSoftwareVersion versions} belonging to this edition. */ diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareTool.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareTool.java index f09bffd792..c6960f450a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareTool.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/InstalledSoftwareTool.java @@ -7,14 +7,10 @@ /** * Represents an installed IDE tool in the global software folder as discovered by the {@code cleanup} commandlet. *

- * Contains a flag indicating whether the tool is marked for deletion and a list of - * {@link InstalledSoftwareEdition editions} belonging to this tool. + * Contains a list of {@link InstalledSoftwareEdition editions} belonging to this tool. */ public class InstalledSoftwareTool extends AbstractInstalledSoftwareItem { - /** A flag indicating whether the tool is marked for deletion. */ - private boolean delete; - /** A list of {@link InstalledSoftwareEdition editions} belonging to this tool. */ private final List editions; @@ -27,36 +23,9 @@ public class InstalledSoftwareTool extends AbstractInstalledSoftwareItem { public InstalledSoftwareTool(String name, Path path) { super(name, path); - this.delete = false; this.editions = new ArrayList<>(); } - /** - * @return {@code true} if this tool is marked for deletion. - */ - public boolean isDelete() { - - return this.delete; - } - - /** - * Sets the deletion flag. - * - * @param delete {@code true} to mark this tool for deletion. - */ - public void setDelete(boolean delete) { - - this.delete = delete; - } - - /** - * @return {@code true} if all editions of this tool are unused. - */ - public boolean isUnused() { - - return this.editions.stream().allMatch(InstalledSoftwareEdition::isUnused); - } - /** * @return the list of {@link InstalledSoftwareEdition editions} belonging to this tool. */ From 26a1049302b631beb5fbd1bec4d8154e8d2dfb4d Mon Sep 17 00:00:00 2001 From: caylipp Date: Thu, 30 Jul 2026 17:26:00 +0200 Subject: [PATCH 24/27] #1953: Centralize IDEasy project discovery - add reusable project discovery to IdeContext - exclude non project folders centrally - reuse project discovery in cleanup and GUI --- .../commandlet/cleanup/CleanupCommandlet.java | 5 +-- .../devonfw/tools/ide/context/IdeContext.java | 44 +++++++++++++++++-- .../ide/gui/context/ProjectManager.java | 15 ++----- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java index ede1baa03a..d03e35b743 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java @@ -58,13 +58,10 @@ private void discoverAndDeleteUnusedSoftware() { List installedSoftwareTools = discoverInstalledSoftware(); // Scan for IDEasy projects - List ideasyProjects = this.context.getFileAccess().listChildren(this.context.getIdeRoot(), Files::isDirectory); + List ideasyProjects = this.context.findProjects(); // Iterate through IDEasy projects and scan software in software folder. Save found software to list for (Path ideasyProject : ideasyProjects) { - if (ideasyProject.getFileName().toString().equals("_ide")) { - continue; - } Path ideasyProjectSoftware = ideasyProject.resolve(IdeContext.FOLDER_SOFTWARE); discoverUsedSoftware(installedSoftwareTools, ideasyProjectSoftware, ideasyProject.getFileName().toString()); discoverUsedSoftware(installedSoftwareTools, ideasyProjectSoftware.resolve(IdeContext.FOLDER_EXTRA), ideasyProject.getFileName().toString()); diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index 3443b60d68..d02df2c4ef 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -1,7 +1,11 @@ package com.devonfw.tools.ide.context; +import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; import com.devonfw.tools.ide.cli.CliAbortException; import com.devonfw.tools.ide.cli.CliException; @@ -30,8 +34,8 @@ import com.devonfw.tools.ide.tool.npm.Npm; import com.devonfw.tools.ide.tool.npm.NpmRepository; import com.devonfw.tools.ide.tool.pip.PipRepository; -import com.devonfw.tools.ide.tool.repository.ToolRepository; import com.devonfw.tools.ide.tool.python.PythonRepository; +import com.devonfw.tools.ide.tool.repository.ToolRepository; import com.devonfw.tools.ide.tool.uv.UvRepository; import com.devonfw.tools.ide.url.model.UrlMetadata; import com.devonfw.tools.ide.variable.IdeVariables; @@ -416,9 +420,41 @@ default void requireOnline(String purpose, boolean explicitOnlineCheck) { Path getIdeRoot(); /** - * @param ideRoot the new value of {@link #getIdeRoot() IDE_ROOT}. Typically detected automatically from the environment and working directory, but may need - * to be set explicitly (e.g. during the initial installation where the {@code IDE_ROOT} environment variable is not yet available but the installation - * target is already known). + * Finds all IDEasy projects below the configured IDE root. + * + * @return the paths of all detected IDEasy projects. + */ + default List findProjects() { + + return findProjects(getIdeRoot()); + } + + /** + * Finds all IDEasy projects below the given IDE root. + * + * @param ideRoot the IDE root containing the IDEasy projects. + * @return the paths of all detected IDEasy projects. + */ + static List findProjects(Path ideRoot) { + + if ((ideRoot == null) || !Files.isDirectory(ideRoot)) { + return List.of(); + } + + try (Stream children = Files.list(ideRoot)) { + return children.filter(Files::isDirectory) + .filter(project -> !FOLDER_UNDERSCORE_IDE.equals(project.getFileName().toString())) + .filter(project -> Files.isDirectory(project.resolve(FOLDER_WORKSPACES))) + .toList(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to find IDEasy projects in " + ideRoot, e); + } + } + + /** + * @param ideRoot the new value of {@link #getIdeRoot() IDE_ROOT}. Typically detected automatically from the environment and working directory, but may + * need to be set explicitly (e.g. during the initial installation where the {@code IDE_ROOT} environment variable is not yet available but the + * installation target is already known). */ void setIdeRoot(Path ideRoot); diff --git a/gui/src/main/java/com/devonfw/ide/gui/context/ProjectManager.java b/gui/src/main/java/com/devonfw/ide/gui/context/ProjectManager.java index a6272ebbb4..68290aa369 100644 --- a/gui/src/main/java/com/devonfw/ide/gui/context/ProjectManager.java +++ b/gui/src/main/java/com/devonfw/ide/gui/context/ProjectManager.java @@ -48,17 +48,10 @@ protected ProjectManager(Path ideRootDirectory) { */ public List getProjectNames() { - try (Stream subPaths = Files.list(ideRootDirectory)) { - return subPaths - .filter(Files::isDirectory) - .map(Path::getFileName) - .map(Path::toString) - .filter(name -> !name.equals(IdeContext.FOLDER_UNDERSCORE_IDE) && Files.exists(ideRootDirectory.resolve(name).resolve("workspaces"))) - .toList(); - - } catch (IOException e) { - throw new RuntimeException("Failed to read project list!", e); - } + return IdeContext.findProjects(this.ideRootDirectory).stream() + .map(Path::getFileName) + .map(Path::toString) + .toList(); } /** From 1890d1b156ca769778ea20589e040152f15f2d05 Mon Sep 17 00:00:00 2001 From: caylipp Date: Fri, 31 Jul 2026 09:19:11 +0200 Subject: [PATCH 25/27] #1953: Fix custom repository cleanup - scan default, Maven, and configured custom repositories explicitly - update cleanup test for the configured custom repository --- .../commandlet/cleanup/CleanupCommandlet.java | 22 ++++++++++++++----- .../ide/commandlet/CleanupCommandletTest.java | 8 +++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java index d03e35b743..755154e80f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java @@ -13,6 +13,8 @@ import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.step.Step; +import com.devonfw.tools.ide.tool.mvn.MvnRepository; +import com.devonfw.tools.ide.tool.repository.ToolRepository; /** * Commandlet which scans your IDE installation for unused software (tools not currently used by any project) and removes them. @@ -74,17 +76,24 @@ private void discoverAndDeleteUnusedSoftware() { } /** - * This method discovers all installed tools in all repositories below $IDE_ROOT/_ide/software and saves them to the list of installed tools. + * Discovers all installed tools in the default, Maven, and custom software repositories. * * @return the list of discovered installed tools. */ private List discoverInstalledSoftware() { + List installedSoftwareTools = new ArrayList<>(); Path softwareRepositoryPath = this.context.getSoftwareRepositoryPath(); - List repositoryFolders = this.context.getFileAccess().listChildren(softwareRepositoryPath, Files::isDirectory); - for (Path repositoryFolder : repositoryFolders) { - discoverInstalledSoftwareRepository(installedSoftwareTools, repositoryFolder); - } + + discoverInstalledSoftwareRepository(installedSoftwareTools, + softwareRepositoryPath.resolve(ToolRepository.ID_DEFAULT)); + + discoverInstalledSoftwareRepository(installedSoftwareTools, + softwareRepositoryPath.resolve(MvnRepository.ID)); + + discoverInstalledSoftwareRepository(installedSoftwareTools, + softwareRepositoryPath.resolve(this.context.getCustomToolRepository().getId())); + return installedSoftwareTools; } @@ -95,6 +104,9 @@ private List discoverInstalledSoftware() { * @param repositoryFolder The software repository folder to scan. */ private void discoverInstalledSoftwareRepository(List installedSoftwareTools, Path repositoryFolder) { + if (!Files.isDirectory(repositoryFolder)) { + return; + } List toolFolders = this.context.getFileAccess().listChildren(repositoryFolder, Files::isDirectory); for (Path toolFolder : toolFolders) { InstalledSoftwareTool tool = new InstalledSoftwareTool(toolFolder.getFileName().toString(), this.context.getFileAccess().toRealPath(toolFolder)); diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java index 8ed6c07e69..54f10375c3 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java @@ -127,13 +127,13 @@ void testCleanupKeepsVersionReferencedByNestedExtraTool() throws IOException { * @throws IOException if the test setup cannot be created. */ @Test - void testCleanupDeletesUnusedSoftwareFromNonDefaultRepository() throws IOException { + void testCleanupDeletesUnusedSoftwareFromCustomRepository() throws IOException { // arrange IdeTestContext context = newContext(PROJECT_BASIC); + String customRepositoryId = context.getCustomToolRepository().getId(); - Path unusedVersion = createInstalledVersion(context, "cleanup-test-repository", "cleanup-test-tool", "default", - "1.0"); + Path unusedVersion = createInstalledVersion(context, customRepositoryId, "cleanup-test-tool", "default", "1.0"); CleanupCommandlet cleanup = getCleanupWithoutConfirmation(context); @@ -142,7 +142,7 @@ void testCleanupDeletesUnusedSoftwareFromNonDefaultRepository() throws IOExcepti // assert assertThat(unusedVersion) - .as("Unused software from a non-default repository should also be discovered and deleted") + .as("Unused software from the configured custom repository should be discovered and deleted") .doesNotExist(); } From 86f291a3da5f7f4a401134955f17612d13894df7 Mon Sep 17 00:00:00 2001 From: Philipp Hoang <115173117+Caylipp@users.noreply.github.com> Date: Fri, 31 Jul 2026 10:17:12 +0200 Subject: [PATCH 26/27] Update cli/src/main/resources/nls/Help.properties Co-authored-by: quando632 --- cli/src/main/resources/nls/Help.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 7ab93dedec..981faf1bab 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -12,7 +12,7 @@ cmd.cdk.detail=The AWS Cloud Development Kit (AWS CDK) is an open-source softwar 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. +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. cmd.complete=Internal commandlet for bash auto-completion. cmd.complete.detail=Run 'ide complete ' to activate the non-interactive autocompletion, replace 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. From eb5970ff59041553afd481874b409249d86f8028 Mon Sep 17 00:00:00 2001 From: caylipp Date: Fri, 31 Jul 2026 13:30:57 +0200 Subject: [PATCH 27/27] #1953: Address cleanup commandlet review feedback - add better help message - propagate user cancellation through the standard abort handling - correct software discovery variable names - report affected tools and editions accurately - cover batch force mode and Windows symlink restrictions - adjust CHANGELOG.adoc entry --- CHANGELOG.adoc | 2 +- .../commandlet/cleanup/CleanupCommandlet.java | 76 +++++++++---------- cli/src/main/resources/nls/Help_de.properties | 2 +- .../ide/commandlet/CleanupCommandletTest.java | 51 +++++++++++-- 4 files changed, 82 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index b773cf769a..42f92cc983 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -9,6 +9,7 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/2176[#2176]: Support 7z archive extraction * https://github.com/devonfw/IDEasy/issues/2100[#2100]: Fix Python not available for Mac x64 * https://github.com/devonfw/IDEasy/issues/2134[#2134]: Add auto-completion for icd command +* https://github.com/devonfw/IDEasy/issues/1953[#1953]: Implement base functionality of cleanup commandlet The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/48?closed=1[milestone 2026.08.001]. @@ -117,7 +118,6 @@ Release with new features and bugfixes: * https://github.com/devonfw/IDEasy/issues/1884[#1884]: Fix java unzipping losing symlink information * https://github.com/devonfw/IDEasy/issues/1716[#1716]: Add commandlet for Claude Code CLI * https://github.com/devonfw/IDEasy/issues/1844[#1844]: Fix vscode installation hanging indefinitely in WSL Linux environments -* https://github.com/devonfw/IDEasy/issues/1953[#1953]: Implement base functionality of cleanup commandlet * https://github.com/devonfw/IDEasy/issues/863[#863]: Mac x64 / ARM android-studio 2024 releases missing — accept .dmg downloads alongside .zip * https://github.com/devonfw/IDEasy/issues/1904[#1904]: Add Inso CLI to IDEasy commandlets * https://github.com/devonfw/IDEasy/issues/1952[#1952]: Ability for platform specific dependencies diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java index 755154e80f..8ffa92648a 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/cleanup/CleanupCommandlet.java @@ -8,7 +8,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.devonfw.tools.ide.cli.CliAbortException; import com.devonfw.tools.ide.commandlet.Commandlet; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.log.IdeLogLevel; @@ -40,6 +39,12 @@ public String getName() { return "cleanup"; } + @Override + public boolean isIdeHomeRequired() { + + return false; + } + @Override protected void doRun() { @@ -64,9 +69,10 @@ private void discoverAndDeleteUnusedSoftware() { // Iterate through IDEasy projects and scan software in software folder. Save found software to list for (Path ideasyProject : ideasyProjects) { + String projectName = ideasyProject.getFileName().toString(); Path ideasyProjectSoftware = ideasyProject.resolve(IdeContext.FOLDER_SOFTWARE); - discoverUsedSoftware(installedSoftwareTools, ideasyProjectSoftware, ideasyProject.getFileName().toString()); - discoverUsedSoftware(installedSoftwareTools, ideasyProjectSoftware.resolve(IdeContext.FOLDER_EXTRA), ideasyProject.getFileName().toString()); + discoverUsedSoftware(installedSoftwareTools, ideasyProjectSoftware, projectName); + discoverUsedSoftware(installedSoftwareTools, ideasyProjectSoftware.resolve(IdeContext.FOLDER_EXTRA), projectName); } // Mark unused software for deletion @@ -109,7 +115,8 @@ private void discoverInstalledSoftwareRepository(List ins } List toolFolders = this.context.getFileAccess().listChildren(repositoryFolder, Files::isDirectory); for (Path toolFolder : toolFolders) { - InstalledSoftwareTool tool = new InstalledSoftwareTool(toolFolder.getFileName().toString(), this.context.getFileAccess().toRealPath(toolFolder)); + Path toolPath = this.context.getFileAccess().toRealPath(toolFolder); + InstalledSoftwareTool tool = new InstalledSoftwareTool(toolFolder.getFileName().toString(), toolPath); installedSoftwareTools.add(tool); discoverInstalledEditions(toolFolder, tool); } @@ -119,15 +126,16 @@ private void discoverInstalledSoftwareRepository(List ins * This method discovers all installed editions of a tool at $IDE_ROOT/_ide/software// and saves them to the edition list of the tool. * Installed versions of the edition are then recursively discovered. * - * @param editionFolder The folder where the editions are saved in ($IDE_ROOT/_ide/software//). + * @param toolFolder The folder where the editions are saved in ($IDE_ROOT/_ide/software//). * @param tool The respective tool for which we are discovering editions. */ - private void discoverInstalledEditions(Path editionFolder, InstalledSoftwareTool tool) { - List subfolders = this.context.getFileAccess().listChildren(editionFolder, Files::isDirectory); - for (Path subfolder : subfolders) { - InstalledSoftwareEdition edition = new InstalledSoftwareEdition(subfolder.getFileName().toString(), this.context.getFileAccess().toRealPath(subfolder)); + private void discoverInstalledEditions(Path toolFolder, InstalledSoftwareTool tool) { + List editionFolders = this.context.getFileAccess().listChildren(toolFolder, Files::isDirectory); + for (Path editionFolder : editionFolders) { + Path editionPath = this.context.getFileAccess().toRealPath(editionFolder); + InstalledSoftwareEdition edition = new InstalledSoftwareEdition(editionFolder.getFileName().toString(), editionPath); tool.getEditions().add(edition); - discoverInstalledVersions(subfolder, edition); + discoverInstalledVersions(editionFolder, edition); } } @@ -135,14 +143,14 @@ private void discoverInstalledEditions(Path editionFolder, InstalledSoftwareTool * This method discovers all installed versions of an edition of a tool at $IDE_ROOT/_ide/software/// and saves them to the version * list of the edition. * - * @param versionFolder The folder where the versions are saved in ($IDE_ROOT/_ide/software///). + * @param editionFolder The folder where the versions are saved in ($IDE_ROOT/_ide/software///). * @param edition The respective edition for which we are discovering versions. */ - private void discoverInstalledVersions(Path versionFolder, InstalledSoftwareEdition edition) { - List subfolders = this.context.getFileAccess().listChildren(versionFolder, Files::isDirectory); - for (Path subfolder : subfolders) { - InstalledSoftwareVersion version = new InstalledSoftwareVersion(subfolder.getFileName().toString(), - this.context.getFileAccess().toRealPath(subfolder)); + private void discoverInstalledVersions(Path editionFolder, InstalledSoftwareEdition edition) { + List versionFolders = this.context.getFileAccess().listChildren(editionFolder, Files::isDirectory); + for (Path versionFolder : versionFolders) { + Path versionPath = this.context.getFileAccess().toRealPath(versionFolder); + InstalledSoftwareVersion version = new InstalledSoftwareVersion(versionFolder.getFileName().toString(), versionPath); edition.getVersions().add(version); } } @@ -226,19 +234,18 @@ private void markUnusedSoftwareForDeletion(List installed } /** - * Generates a summary report for tools, editions, and versions to be deleted and prompts the user for confirmation. If the user agrees, we proceed with - * deletion of the unused tools, editions, and versions. + * Generates a summary report for software versions to be deleted and prompts the user for confirmation. If the user agrees, the unused software versions and + * empty parent folders are deleted. * - * @param installedSoftwareTools the list of installed tools with deletion flags set. + * @param installedSoftwareTools the list of installed tools containing versions with deletion flags. */ private void logSoftwareToBeDeleted(List installedSoftwareTools) { String logOutput = ""; - int totalToolsDeleted = 0; - int totalEditionsDeleted = 0; + int totalAffectedTools = 0; + int totalAffectedEditions = 0; int totalVersionsDeleted = 0; for (InstalledSoftwareTool tool : installedSoftwareTools) { String logOutputEdition = ""; - int editionsDeleted = 0; for (InstalledSoftwareEdition edition : tool.getEditions()) { String logOutputVersion = ""; int versionsDeleted = 0; @@ -255,35 +262,24 @@ private void logSoftwareToBeDeleted(List installedSoftwar logOutputVersion += "\t\t + " + (edition.getVersions().size() - versionsDeleted) + " more version(s) of this edition will not be deleted\n"; } logOutputEdition += "\t - " + edition.getName() + "\n" + logOutputVersion; - editionsDeleted++; - totalEditionsDeleted++; + totalAffectedEditions++; } } if (!logOutputEdition.isBlank()) { - // If at least one edition of the tool should have a delete operation - if (editionsDeleted < tool.getEditions().size()) { - logOutputEdition += "\t + " + (tool.getEditions().size() - editionsDeleted) + " more edition(s) of this tool will not be deleted\n"; - } logOutput += " - " + tool.getName() + "\n" + logOutputEdition; - totalToolsDeleted++; + totalAffectedTools++; } } if (logOutput.isBlank()) { LOG.info("No installed tools will be deleted. All installed software is used by at least one project."); } else { - LOG.info("The following installed tools will be deleted: \n" + logOutput); - LOG.info("Summary: {} installed tool versions across {} editions of {} tools will be deleted.", totalVersionsDeleted, totalEditionsDeleted, - totalToolsDeleted); + LOG.info("The following installed tool versions will be deleted: \n" + logOutput); + LOG.info("Summary: {} installed tool versions across {} affected editions of {} affected tools will be deleted.", totalVersionsDeleted, + totalAffectedEditions, totalAffectedTools); - // Ask for confirmation. Automatically confirmed if the global force option is provided. - try { - this.context.askToContinue("Do you want to continue?"); - - } catch (CliAbortException e) { - LOG.info("Installed Tools will not be deleted."); - return; - } + // Ask for confirmation. Automatically confirmed in batch mode with the global force option. + this.context.askToContinue("Do you want to continue?"); deleteUnusedSoftware(installedSoftwareTools); } } diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index f03cd2cd34..16132a0726 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -12,7 +12,7 @@ cmd.cdk.detail=Das AWS Cloud Development Kit (AWS CDK) ist ein Open-Source-Softw 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. +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. cmd.complete=Internes Werkzeug für bash Autovervollständigung. cmd.complete.detail=Geben Sie 'ide complete ' in die Konsole ein um die einfache Autovervollständigung zu aktivieren, ersetzen Sie 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. diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java index 54f10375c3..e032ae9cce 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/CleanupCommandletTest.java @@ -10,6 +10,7 @@ import com.devonfw.tools.ide.context.AbstractIdeContextTest; import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.io.WindowsSymlinkTestHelper; /** * Test of {@link CleanupCommandlet}. @@ -26,6 +27,8 @@ class CleanupCommandletTest extends AbstractIdeContextTest { @Test void testCleanupDeletesUnusedAndKeepsUsedGlobalSoftware() throws IOException { + WindowsSymlinkTestHelper.assumeSymlinksSupported(); + // arrange IdeTestContext context = newContext(PROJECT_BASIC); @@ -37,7 +40,7 @@ void testCleanupDeletesUnusedAndKeepsUsedGlobalSoftware() throws IOException { createSoftwareLink(projectSoftware.resolve("cleanup-test-java"), usedVersion); - CleanupCommandlet cleanup = getCleanupWithoutConfirmation(context); + CleanupCommandlet cleanup = getCleanupWithConfirmation(context); // act cleanup.run(); @@ -58,6 +61,8 @@ void testCleanupDeletesUnusedAndKeepsUsedGlobalSoftware() throws IOException { @Test void testCleanupKeepsVersionReferencedBySubdirectory() throws IOException { + WindowsSymlinkTestHelper.assumeSymlinksSupported(); + // arrange IdeTestContext context = newContext(PROJECT_BASIC); @@ -72,7 +77,7 @@ void testCleanupKeepsVersionReferencedBySubdirectory() throws IOException { createSoftwareLink(projectSoftware.resolve("cleanup-test-macos"), macOsFolder); - CleanupCommandlet cleanup = getCleanupWithoutConfirmation(context); + CleanupCommandlet cleanup = getCleanupWithConfirmation(context); // act cleanup.run(); @@ -93,6 +98,8 @@ void testCleanupKeepsVersionReferencedBySubdirectory() throws IOException { @Test void testCleanupKeepsVersionReferencedByNestedExtraTool() throws IOException { + WindowsSymlinkTestHelper.assumeSymlinksSupported(); + // arrange IdeTestContext context = newContext(PROJECT_BASIC); @@ -108,7 +115,7 @@ void testCleanupKeepsVersionReferencedByNestedExtraTool() throws IOException { createSoftwareLink(nestedExtraToolLink, usedVersion); - CleanupCommandlet cleanup = getCleanupWithoutConfirmation(context); + CleanupCommandlet cleanup = getCleanupWithConfirmation(context); // act cleanup.run(); @@ -122,7 +129,7 @@ void testCleanupKeepsVersionReferencedByNestedExtraTool() throws IOException { } /** - * Tests that unused installations in repositories other than {@code default} are also discovered and deleted. + * Tests that unused installations in the configured custom repository are discovered and deleted. * * @throws IOException if the test setup cannot be created. */ @@ -135,7 +142,7 @@ void testCleanupDeletesUnusedSoftwareFromCustomRepository() throws IOException { Path unusedVersion = createInstalledVersion(context, customRepositoryId, "cleanup-test-tool", "default", "1.0"); - CleanupCommandlet cleanup = getCleanupWithoutConfirmation(context); + CleanupCommandlet cleanup = getCleanupWithConfirmation(context); // act cleanup.run(); @@ -146,6 +153,36 @@ void testCleanupDeletesUnusedSoftwareFromCustomRepository() throws IOException { .doesNotExist(); } + /** + * Tests that batch mode combined with force mode skips the confirmation prompt. + * + * @throws IOException if the test setup cannot be created. + */ + @Test + void testCleanupSkipsConfirmationInBatchForceMode() throws IOException { + + // arrange + IdeTestContext context = newContext(PROJECT_BASIC); + + Path unusedVersion = createInstalledVersion( + context, "default", "cleanup-test-force", "default", "1.0"); + + context.getStartContext().setBatchMode(true); + context.getStartContext().setForceMode(true); + + CleanupCommandlet cleanup = context.getCommandletManager().getCommandlet(CleanupCommandlet.class); + + // act + cleanup.run(); + + // assert + assertThat(unusedVersion) + .as("Unused software should be deleted without an interactive confirmation in batch force mode") + .doesNotExist(); + + assertThat(context).logAtSuccess().hasMessage("Unused tools have been deleted successfully."); + } + /** * Creates an installed software version with the structure {@code _ide/software////}. * @@ -188,12 +225,12 @@ private void createSoftwareLink(Path link, Path target) throws IOException { } /** - * Gets the cleanup commandlet and configures the confirmation answer. + * Gets the cleanup commandlet and configures an affirmative confirmation answer. * * @param context the test context. * @return the configured cleanup commandlet. */ - private CleanupCommandlet getCleanupWithoutConfirmation(IdeTestContext context) { + private CleanupCommandlet getCleanupWithConfirmation(IdeTestContext context) { context.setAnswers("yes"); return context.getCommandletManager().getCommandlet(CleanupCommandlet.class);