diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java index 8c171e327..bb96fe882 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManager.java @@ -39,7 +39,8 @@ public interface CommandletManager { /** * @param name the {@link Commandlet#getName() name} of the requested {@link ToolCommandlet}. - * @return the requested {@link ToolCommandlet} or {@code null} if not found. + * @return the requested {@link ToolCommandlet} if found. + * @throws IllegalArgumentException if the commandlet with the given name is not a {@link ToolCommandlet} */ default ToolCommandlet getToolCommandlet(String name) { @@ -50,4 +51,16 @@ default ToolCommandlet getToolCommandlet(String name) { throw new IllegalArgumentException("The commandlet " + name + " is not a ToolCommandlet!"); } + /** + * @param name the {@link Commandlet#getName() name} of the requested {@link ToolCommandlet}. + * @return the requested {@link ToolCommandlet} or {@code null} if not found. + */ + default ToolCommandlet getToolCommandletOrNull(String name) { + + Commandlet commandlet = getCommandlet(name); + if (commandlet instanceof ToolCommandlet) { + return (ToolCommandlet) commandlet; + } + return null; + } } 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 122ea7842..17b02a0a1 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 @@ -70,6 +70,8 @@ public CommandletManagerImpl(IdeContext context) { add(new EditionListCommandlet(context)); add(new VersionCommandlet(context)); add(new RepositoryCommandlet(context)); + add(new UpdateCommandlet(context)); + add(new CreateCommandlet(context)); add(new Gh(context)); add(new Helm(context)); add(new Java(context)); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java new file mode 100644 index 000000000..82f52b553 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -0,0 +1,77 @@ +package com.devonfw.tools.ide.commandlet; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.io.FileAccess; +import com.devonfw.tools.ide.process.ProcessContext; +import com.devonfw.tools.ide.process.ProcessResult; +import com.devonfw.tools.ide.property.StringProperty; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * {@link Commandlet} to create a new IDEasy instance + */ +public class CreateCommandlet extends Commandlet { + + private final StringProperty newInstance; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public CreateCommandlet(IdeContext context) { + + super(context); + addKeyword(getName()); + newInstance = add(new StringProperty("", false, "newInstance")); + } + + @Override + public String getName() { + + return "create"; + } + + @Override + public void run() { + + String newInstanceName = newInstance.getValue(); + Path newInstancePath; + + if (newInstanceName == null) { + newInstancePath = this.context.getCwd(); + } else { + newInstancePath = this.context.getIdeRoot().resolve(newInstanceName); + this.context.getFileAccess().mkdirs(newInstancePath); + } + + this.context.info("Creating new IDEasy instance in {}", newInstancePath); + if (!this.context.getFileAccess().isEmptyDir(newInstancePath)) { + this.context.askToContinue("Directory is not empty, continue?"); + } + + initializeInstance(newInstancePath); + ProcessContext pc = this.context.newProcess().executable("ideasy"); + pc.addArgs("update"); + pc.directory(newInstancePath); + if (pc.run() == ProcessResult.SUCCESS) { + this.context.success("IDEasy Instance successfully created in {}", newInstancePath); + } else { + this.context.warning("Could not create IDEasy Instance."); + } + } + + private void initializeInstance(Path newInstancePath) { + + FileAccess fileAccess = this.context.getFileAccess(); + fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_SOFTWARE)); + fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_UPDATES)); + fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_PLUGINS)); + fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_WORKSPACES)); + fileAccess.mkdirs(newInstancePath.resolve(IdeContext.FOLDER_SETTINGS)); + + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpdateCommandlet.java new file mode 100644 index 000000000..9e82ff29a --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/UpdateCommandlet.java @@ -0,0 +1,169 @@ +package com.devonfw.tools.ide.commandlet; + +import com.devonfw.tools.ide.common.StepContainer; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.property.StringProperty; +import com.devonfw.tools.ide.repo.CustomTool; +import com.devonfw.tools.ide.tool.CustomToolCommandlet; +import com.devonfw.tools.ide.tool.ToolCommandlet; +import com.devonfw.tools.ide.variable.IdeVariables; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Stream; + +/** + * {@link Commandlet} to update settings, software and repositories + */ +public class UpdateCommandlet extends Commandlet { + + private static final String DEFAULT_SETTINGS_REPO_URL = "https://github.com/devonfw/ide-settings"; + + private final StringProperty settingsRepo; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public UpdateCommandlet(IdeContext context) { + + super(context); + addKeyword(getName()); + settingsRepo = add(new StringProperty("", false, "settingsRepository")); + } + + @Override + public String getName() { + + return "update"; + } + + @Override + public void run() { + + updateSettings(); + this.context.getFileAccess().mkdirs(this.context.getWorkspacePath()); + Path templatesFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_TEMPLATES); + if (!Files.exists(templatesFolder)) { + Path legacyTemplatesFolder = this.context.getSettingsPath().resolve(IdeContext.FOLDER_LEGACY_TEMPLATES); + if (Files.exists(legacyTemplatesFolder)) { + templatesFolder = legacyTemplatesFolder; + } else { + this.context.warning("Templates folder is missing in settings folder."); + return; + } + } + setupConf(templatesFolder, this.context.getIdeHome()); + updateSoftware(); + updateRepositories(); + } + + private void setupConf(Path template, Path conf) { + + List<Path> children = this.context.getFileAccess().listChildren(template, f -> true); + for (Path child : children) { + + String basename = child.getFileName().toString(); + Path confPath = conf.resolve(basename); + + if (Files.isDirectory(child)) { + if (!Files.isDirectory(confPath)) { + this.context.getFileAccess().mkdirs(confPath); + } + setupConf(child, confPath); + } else if (Files.isRegularFile(child)) { + if (Files.isRegularFile(confPath)) { + this.context.debug("Configuration {} already exists - skipping to copy from {}", confPath, child); + } else { + if (!basename.equals("settings.xml")) { + this.context.info("Copying template {} to {}.", child, confPath); + this.context.getFileAccess().copy(child, confPath); + } + } + } + } + } + + private void updateSettings() { + + this.context.info("Updating settings repository ..."); + Path settingsPath = this.context.getSettingsPath(); + if (Files.isDirectory(settingsPath) && !this.context.getFileAccess().isEmptyDir(settingsPath)) { + // perform git pull on the settings repo + this.context.getGitContext().pull(settingsPath); + this.context.success("Successfully updated settings repository."); + } else { + // check if a settings repository is given then clone, otherwise prompt user for a repository. + String repository = settingsRepo.getValue(); + if (repository == null) { + if (this.context.isBatchMode()) { + repository = DEFAULT_SETTINGS_REPO_URL; + } else { + this.context.info("Missing your settings at {} and no SETTINGS_URL is defined.", settingsPath); + this.context.info("Further details can be found here:"); + this.context.info("https://github.com/devonfw/IDEasy/blob/main/documentation/settings.asciidoc"); + this.context.info("Please contact the technical lead of your project to get the SETTINGS_URL for your project."); + this.context.info("In case you just want to test IDEasy you may simply hit return to install the default settings."); + this.context.info(""); + this.context.info("Settings URL [{}]:", DEFAULT_SETTINGS_REPO_URL); + repository = this.context.readLine(); + } + } + if (repository.isBlank()) { + repository = DEFAULT_SETTINGS_REPO_URL; + } + this.context.getGitContext().pullOrClone(repository, settingsPath); + this.context.success("Successfully cloned settings repository."); + } + } + + private void updateSoftware() { + + Set<ToolCommandlet> toolCommandlets = new HashSet<>(); + + // installed tools in IDE_HOME/software + List<Path> softwares = this.context.getFileAccess().listChildren(this.context.getSoftwarePath(), Files::isDirectory); + for (Path software : softwares) { + String toolName = software.getFileName().toString(); + ToolCommandlet toolCommandlet = this.context.getCommandletManager().getToolCommandletOrNull(toolName); + if (toolCommandlet != null) { + toolCommandlets.add(toolCommandlet); + } + } + + // regular tools in $IDE_TOOLS + List<String> regularTools = IdeVariables.IDE_TOOLS.get(this.context); + if (regularTools != null) { + for (String regularTool : regularTools) { + toolCommandlets.add(this.context.getCommandletManager().getToolCommandlet(regularTool)); + } + } + + // custom tools + for (CustomTool customTool : this.context.getCustomToolRepository().getTools()) { + CustomToolCommandlet customToolCommandlet = new CustomToolCommandlet(this.context, customTool); + toolCommandlets.add(customToolCommandlet); + } + + // update/install the toolCommandlets + StepContainer container = new StepContainer(this.context); + for (ToolCommandlet toolCommandlet : toolCommandlets) { + try { + container.startStep(toolCommandlet.getName()); + toolCommandlet.install(false); + container.endStep(toolCommandlet.getName(), true, null); + } catch (Exception e) { + container.endStep(toolCommandlet.getName(), false, e); + } + } + // summary + container.complete(); + } + + private void updateRepositories() { + this.context.getCommandletManager().getCommandlet(RepositoryCommandlet.class).run(); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/common/StepContainer.java b/cli/src/main/java/com/devonfw/tools/ide/common/StepContainer.java new file mode 100644 index 000000000..bd11e1e91 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/common/StepContainer.java @@ -0,0 +1,89 @@ +package com.devonfw.tools.ide.common; + +import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.context.IdeContext; + +import java.util.ArrayList; +import java.util.List; + +/** + * A utility class to manage and log the progress of steps in a process. + * Each step can be started, ended with success or failure, and the overall completion + * status can be checked. + * @throws CliException if one or more steps fail. + */ +public class StepContainer { + + private final IdeContext context; + + /** List of steps that ended successfully. */ + private List<String> successfulSteps; + + /** List of steps that failed. */ + private List<String> failedSteps; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public StepContainer(IdeContext context) { + + this.context = context; + successfulSteps = new ArrayList<>(); + failedSteps = new ArrayList<>(); + } + + /** + * Logs the start of a step. + * + * @param stepName the name of the step. + */ + public void startStep(String stepName) { + + this.context.step("Starting step: {}", stepName); + } + + /** + * Logs the end of a step, indicating success or failure. + * + * @param stepName the name of the step. + * @param success {@code true} if the step succeeded, {@code false} otherwise. + * @param e the exception associated with the failure, or {@code null} if the step succeeded. + */ + public void endStep(String stepName, boolean success, Throwable e) { + + if (success) { + successfulSteps.add(stepName); + this.context.success("Step '{}' succeeded.", stepName); + } else { + failedSteps.add(stepName); + this.context.warning("Step '{}' failed.", stepName); + if (e != null) { + this.context.error(e); + } + } + } + + /** + * Checks the overall completion status of all steps. + * + * @throws CliException if one or more steps fail, providing a detailed summary. + */ + public void complete() { + + if (failedSteps.isEmpty()) { + this.context.success("All {} steps ended successfully!", successfulSteps.size()); + } else { + throw new CliException(String.format("%d step(s) failed (%d%%) and %d step(s) succeeded (%d%%) out of %d step(s)!", + failedSteps.size(), calculatePercentage(failedSteps.size()), successfulSteps.size(), + 100 - calculatePercentage(failedSteps.size()), successfulSteps.size() + failedSteps.size())); + } + } + + private int calculatePercentage(int count) { + + return (count * 100) / (successfulSteps.size() + failedSteps.size()); + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index 30c995813..b9ba8b1eb 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -688,7 +688,7 @@ public <O> O question(String question, O... options) { /** * @return the input from the end-user (e.g. read from the console). */ - protected abstract String readLine(); + public abstract String readLine(); private static <O> void addMapping(Map<String, O> mapping, String key, O option) { 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 4090da8bb..21415c626 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 @@ -118,6 +118,10 @@ public interface IdeContext extends IdeLogger { /** The default for {@link #getWorkspaceName()}. */ String WORKSPACE_MAIN = "main"; + String FOLDER_TEMPLATES = "templates"; + + String FOLDER_LEGACY_TEMPLATES = "devon"; + /** * @return {@code true} in case of quiet mode (reduced output), {@code false} otherwise. */ @@ -177,6 +181,11 @@ default boolean question(String question) { @SuppressWarnings("unchecked") <O> O question(String question, O... options); + /** + * @return the input from the end-user (e.g. read from the console). + */ + String readLine(); + /** * Will ask the given question. If the user answers with "yes" the method will return and the process can continue. * Otherwise if the user answers with "no" an exception is thrown to abort further processing. diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java index aa56c36d6..a9e1cc77d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContextConsole.java @@ -36,7 +36,7 @@ public IdeContextConsole(IdeLogLevel minLogLevel, Appendable out, boolean colore } @Override - protected String readLine() { + public String readLine() { if (this.scanner == null) { return System.console().readLine(); diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java index 8b1260083..edcbe652d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java @@ -232,4 +232,10 @@ default void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtrac */ Path findExistingFile(String fileName, List<Path> searchDirs); + /** + * Checks if the given directory is empty. + * @param dir The {@link Path} object representing the directory to check. + * @return {@code true} if the directory is empty, {@code false} otherwise. + */ + boolean isEmptyDir(Path dir); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java index 7e03e4a76..9e3ba0f1b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java @@ -794,6 +794,14 @@ public List<Path> listChildren(Path dir, Predicate<Path> filter) { return children; } + @Override + public boolean isEmptyDir(Path dir) { + + return listChildren(dir, f -> true).isEmpty(); + } + + + @Override public Path findExistingFile(String fileName, List<Path> searchDirs) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepositoryImpl.java b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepositoryImpl.java index 584535d7f..261b180be 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepositoryImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/repo/CustomToolRepositoryImpl.java @@ -189,7 +189,7 @@ private static boolean getBoolean(JsonObject json, String property, Boolean defa } return defaultValue.booleanValue(); } - ValueType valueType = json.getValueType(); + ValueType valueType = value.getValueType(); if (valueType == ValueType.TRUE) { return true; } else if (valueType == ValueType.FALSE) { @@ -213,8 +213,8 @@ private static String getString(JsonObject json, String property, String default } return defaultValue; } - require(json, ValueType.STRING); - return ((JsonString) json).getString(); + require(value, ValueType.STRING); + return ((JsonString) value).getString(); } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/CustomToolCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/tool/CustomToolCommandlet.java new file mode 100644 index 000000000..1e363cc55 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/CustomToolCommandlet.java @@ -0,0 +1,35 @@ +package com.devonfw.tools.ide.tool; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.repo.CustomTool; +import com.devonfw.tools.ide.version.VersionIdentifier; + + +public class CustomToolCommandlet extends LocalToolCommandlet { + + private CustomTool customTool; + + public CustomToolCommandlet(IdeContext context, CustomTool customTool) { + + super(context, customTool.getTool(), null); + this.customTool = customTool; + } + + @Override + public ToolInstallation installInRepo(VersionIdentifier version) { + + return installInRepo(version, this.customTool.getEdition()); + } + + @Override + public ToolInstallation installInRepo(VersionIdentifier version, String edition) { + + return installInRepo(version, edition, this.context.getCustomToolRepository()); + } + + @Override + public VersionIdentifier getConfiguredVersion() { + + return this.customTool.getVersion(); + } +} diff --git a/cli/src/main/resources/nls/Ide.properties b/cli/src/main/resources/nls/Ide.properties index 1715ce61a..3f96edddd 100644 --- a/cli/src/main/resources/nls/Ide.properties +++ b/cli/src/main/resources/nls/Ide.properties @@ -12,10 +12,12 @@ cmd-get-edition=Get the edition of the selected tool. cmd-get-version=Get the version of the selected tool. cmd-gh=Tool commandlet for Github CLI. cmd-repository=setup the pre-configured git repository. +cmd-create=create a new IDEasy instance. cmd-gradle=Tool commandlet for Gradle (Build-Tool) cmd-helm=Tool commandlet for Helm (Kubernetes Package Manager) cmd-help=Prints this help. cmd-install=Install the selected tool. +cmd-update=update settings, software and repositories. cmd-java=Tool commandlet for Java (OpenJDK) cmd-jmc=Tool commandlet for JDK Mission Control cmd-kotlinc=Tool commandlet for Kotlin. @@ -46,6 +48,7 @@ val-tool=The tool commandlet to select. val-version=The tool version val-set-version-version=The tool version to set. val-repository-repository=The name of the properties file of the pre-configured git repository to setup, omit to setup all active repositories. +val-create-newInstance=The name of the new instance that will be created. version-banner=Current version of IDE is {} opt--batch=enable batch mode (non-interactive) opt--debug=enable debug logging diff --git a/cli/src/main/resources/nls/Ide_de.properties b/cli/src/main/resources/nls/Ide_de.properties index 2d80f673e..1257a568b 100644 --- a/cli/src/main/resources/nls/Ide_de.properties +++ b/cli/src/main/resources/nls/Ide_de.properties @@ -14,6 +14,7 @@ cmd-repository=Richtet das vorkonfigurierte Git Repository ein. cmd-helm=Werkzeug Kommando für Helm (Kubernetes Package Manager) cmd-help=Zeigt diese Hilfe an. cmd-install=Installiert das selektierte Werkzeug. +cmd-update=Updatet die Settings, Software und Repositories. cmd-java=Werkzeug Kommando für Java (OpenJDK) cmd-jmc=Werkzeug Kommando für JDK Mission Control cmd-kotlinc=Werkzeug Kommando für Kotlin. diff --git a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java index 0cfa42719..0f00eb8b9 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java +++ b/cli/src/test/java/com/devonfw/tools/ide/context/AbstractIdeTestContext.java @@ -51,7 +51,7 @@ public boolean isTest() { } @Override - protected String readLine() { + public String readLine() { if (this.answerIndex >= this.answers.length) { throw new IllegalStateException("End of answers reached!");