• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 22241505980

20 Feb 2026 09:16PM UTC coverage: 70.656% (+0.2%) from 70.474%
22241505980

Pull #1710

github

web-flow
Merge 04e4bdacd into 379acdc9d
Pull Request #1710: #404: allow logging via SLF4J

4121 of 6440 branches covered (63.99%)

Branch coverage included in aggregate %.

10704 of 14542 relevant lines covered (73.61%)

3.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

72.27
cli/src/main/java/com/devonfw/tools/ide/tool/IdeasyCommandlet.java
1
package com.devonfw.tools.ide.tool;
2

3
import java.io.Reader;
4
import java.io.Writer;
5
import java.nio.file.Files;
6
import java.nio.file.Path;
7
import java.util.ArrayList;
8
import java.util.Iterator;
9
import java.util.List;
10
import java.util.Set;
11

12
import org.slf4j.Logger;
13
import org.slf4j.LoggerFactory;
14

15
import com.devonfw.tools.ide.cli.CliException;
16
import com.devonfw.tools.ide.commandlet.UpgradeMode;
17
import com.devonfw.tools.ide.common.SimpleSystemPath;
18
import com.devonfw.tools.ide.common.Tag;
19
import com.devonfw.tools.ide.context.IdeContext;
20
import com.devonfw.tools.ide.io.FileAccess;
21
import com.devonfw.tools.ide.io.ini.IniFile;
22
import com.devonfw.tools.ide.io.ini.IniSection;
23
import com.devonfw.tools.ide.log.IdeLogLevel;
24
import com.devonfw.tools.ide.os.WindowsHelper;
25
import com.devonfw.tools.ide.os.WindowsPathSyntax;
26
import com.devonfw.tools.ide.process.ProcessMode;
27
import com.devonfw.tools.ide.process.ProcessResult;
28
import com.devonfw.tools.ide.tool.mvn.MvnArtifact;
29
import com.devonfw.tools.ide.tool.mvn.MvnBasedLocalToolCommandlet;
30
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
31
import com.devonfw.tools.ide.variable.IdeVariables;
32
import com.devonfw.tools.ide.version.IdeVersion;
33
import com.devonfw.tools.ide.version.VersionIdentifier;
34
import com.fasterxml.jackson.databind.JsonNode;
35
import com.fasterxml.jackson.databind.ObjectMapper;
36
import com.fasterxml.jackson.databind.node.ArrayNode;
37
import com.fasterxml.jackson.databind.node.ObjectNode;
38

39
/**
40
 * {@link MvnBasedLocalToolCommandlet} for IDEasy (ide-cli).
41
 */
42
public class IdeasyCommandlet extends MvnBasedLocalToolCommandlet {
43

44
  private static final Logger LOG = LoggerFactory.getLogger(IdeasyCommandlet.class);
3✔
45

46
  /** The {@link MvnArtifact} for IDEasy. */
47
  public static final MvnArtifact ARTIFACT = MvnArtifact.ofIdeasyCli("*!", "tar.gz", "${os}-${arch}");
6✔
48

49
  private static final String BASH_CODE_SOURCE_FUNCTIONS = "source \"$IDE_ROOT/_ide/installation/functions\"";
50

51
  /** The {@link #getName() tool name}. */
52
  public static final String TOOL_NAME = "ideasy";
53
  public static final String BASHRC = ".bashrc";
54
  public static final String ZSHRC = ".zshrc";
55
  public static final String IDE_BIN = "\\_ide\\bin";
56
  public static final String IDE_INSTALLATION_BIN = "\\_ide\\installation\\bin";
57

58
  private final UpgradeMode mode;
59

60
  /**
61
   * The constructor.
62
   *
63
   * @param context the {@link IdeContext}.
64
   */
65
  public IdeasyCommandlet(IdeContext context) {
66
    this(context, UpgradeMode.STABLE);
4✔
67
  }
1✔
68

69
  /**
70
   * The constructor.
71
   *
72
   * @param context the {@link IdeContext}.
73
   * @param mode the {@link UpgradeMode}.
74
   */
75
  public IdeasyCommandlet(IdeContext context, UpgradeMode mode) {
76

77
    super(context, TOOL_NAME, ARTIFACT, Set.of(Tag.PRODUCTIVITY, Tag.IDE));
8✔
78
    this.mode = mode;
3✔
79
  }
1✔
80

81
  @Override
82
  public VersionIdentifier getInstalledVersion() {
83

84
    return IdeVersion.getVersionIdentifier();
2✔
85
  }
86

87
  @Override
88
  public String getInstalledEdition() {
89

90
    return this.tool;
3✔
91
  }
92

93
  @Override
94
  public String getConfiguredEdition() {
95

96
    return this.tool;
3✔
97
  }
98

99
  @Override
100
  public VersionIdentifier getConfiguredVersion() {
101

102
    UpgradeMode upgradeMode = this.mode;
3✔
103
    if (upgradeMode == null) {
2!
104
      if (IdeVersion.isSnapshot()) {
2!
105
        upgradeMode = UpgradeMode.SNAPSHOT;
3✔
106
      } else {
107
        if (IdeVersion.getVersionIdentifier().getDevelopmentPhase().isStable()) {
×
108
          upgradeMode = UpgradeMode.STABLE;
×
109
        } else {
110
          upgradeMode = UpgradeMode.UNSTABLE;
×
111
        }
112
      }
113
    }
114
    return upgradeMode.getVersion();
3✔
115
  }
116

117
  @Override
118
  public Path getToolPath() {
119

120
    return this.context.getIdeInstallationPath();
4✔
121
  }
122

123
  @Override
124
  protected ToolInstallation doInstall(ToolInstallRequest request) {
125

126
    this.context.requireOnline("upgrade of IDEasy", true);
5✔
127

128
    if (IdeVersion.isUndefined() && !this.context.isForceMode()) {
6!
129
      VersionIdentifier version = IdeVersion.getVersionIdentifier();
2✔
130
      LOG.warn("You are using IDEasy version {} which indicates local development - skipping upgrade.", version);
4✔
131
      return toolAlreadyInstalled(request);
4✔
132
    }
133
    return super.doInstall(request);
×
134
  }
135

136
  /**
137
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
138
   */
139
  public VersionIdentifier getLatestVersion() {
140

141
    if (!this.context.isForceMode()) {
4!
142
      VersionIdentifier currentVersion = IdeVersion.getVersionIdentifier();
2✔
143
      if (IdeVersion.isUndefined()) {
2!
144
        return currentVersion;
2✔
145
      }
146
    }
147
    VersionIdentifier configuredVersion = getConfiguredVersion();
×
148
    return getToolRepository().resolveVersion(this.tool, getConfiguredEdition(), configuredVersion, this);
×
149
  }
150

151
  /**
152
   * Checks if an update is available and logs according information.
153
   *
154
   * @return {@code true} if an update is available, {@code false} otherwise.
155
   */
156
  public boolean checkIfUpdateIsAvailable() {
157
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
158
    LOG.info(IdeLogLevel.SUCCESS.getSlf4jMarker(), "Your version of IDEasy is {}.", installedVersion);
6✔
159
    if (IdeVersion.isSnapshot()) {
2!
160
      LOG.warn("You are using a SNAPSHOT version of IDEasy. For stability consider switching to a stable release via 'ide upgrade --mode=stable'");
3✔
161
    }
162
    if (this.context.isOffline()) {
4✔
163
      LOG.warn("Skipping check for newer version of IDEasy because you are offline.");
3✔
164
      return false;
2✔
165
    }
166
    VersionIdentifier latestVersion = getLatestVersion();
3✔
167
    if (installedVersion.equals(latestVersion)) {
4!
168
      LOG.info(IdeLogLevel.SUCCESS.getSlf4jMarker(), "Your are using the latest version of IDEasy and no update is available.");
5✔
169
      return false;
2✔
170
    } else {
171
      LOG.info(IdeLogLevel.INTERACTION.getSlf4jMarker(),
×
172
          "Your version of IDEasy is {} but version {} is available. Please run the following command to upgrade to the latest version:\n"
173
              + "ide upgrade", installedVersion, latestVersion);
174
      return true;
×
175
    }
176
  }
177

178
  /**
179
   * Initial installation of IDEasy.
180
   *
181
   * @param cwd the {@link Path} to the current working directory.
182
   * @see com.devonfw.tools.ide.commandlet.InstallCommandlet
183
   */
184
  public void installIdeasy(Path cwd) {
185
    Path ideRoot = determineIdeRoot(cwd);
4✔
186
    Path idePath = ideRoot.resolve(IdeContext.FOLDER_UNDERSCORE_IDE);
4✔
187
    Path installationPath = idePath.resolve(IdeContext.FOLDER_INSTALLATION);
4✔
188
    Path ideasySoftwarePath = idePath.resolve(IdeContext.FOLDER_SOFTWARE).resolve(MvnRepository.ID).resolve(IdeasyCommandlet.TOOL_NAME)
8✔
189
        .resolve(IdeasyCommandlet.TOOL_NAME);
2✔
190
    Path ideasyVersionPath = ideasySoftwarePath.resolve(IdeVersion.getVersionString());
4✔
191
    FileAccess fileAccess = this.context.getFileAccess();
4✔
192
    if (Files.isDirectory(ideasyVersionPath)) {
5!
193
      LOG.error("IDEasy is already installed at {} - if your installation is broken, delete it manually and rerun setup!", ideasyVersionPath);
×
194
    } else {
195
      List<Path> installationArtifacts = new ArrayList<>();
4✔
196
      boolean success = true;
2✔
197
      success &= addInstallationArtifact(cwd, "bin", true, installationArtifacts);
9✔
198
      success &= addInstallationArtifact(cwd, "functions", true, installationArtifacts);
9✔
199
      success &= addInstallationArtifact(cwd, "internal", true, installationArtifacts);
9✔
200
      success &= addInstallationArtifact(cwd, "system", true, installationArtifacts);
9✔
201
      success &= addInstallationArtifact(cwd, "IDEasy.pdf", true, installationArtifacts);
9✔
202
      success &= addInstallationArtifact(cwd, "setup", true, installationArtifacts);
9✔
203
      success &= addInstallationArtifact(cwd, "setup.bat", false, installationArtifacts);
9✔
204
      if (!success) {
2!
205
        throw new CliException("IDEasy release is inconsistent at " + cwd);
×
206
      }
207
      fileAccess.mkdirs(ideasyVersionPath);
3✔
208
      for (Path installationArtifact : installationArtifacts) {
10✔
209
        fileAccess.copy(installationArtifact, ideasyVersionPath);
4✔
210
      }
1✔
211
      this.context.writeVersionFile(IdeVersion.getVersionIdentifier(), ideasyVersionPath);
5✔
212
    }
213
    fileAccess.symlink(ideasyVersionPath, installationPath);
4✔
214
    addToShellRc(BASHRC, ideRoot, null);
5✔
215
    addToShellRc(ZSHRC, ideRoot, "autoload -U +X bashcompinit && bashcompinit");
5✔
216
    installIdeasyWindowsEnv(ideRoot, installationPath);
4✔
217
    LOG.info(IdeLogLevel.SUCCESS.getSlf4jMarker(), "IDEasy has been installed successfully on your system.");
5✔
218
    LOG.warn("IDEasy has been setup for new shells but it cannot work in your current shell(s).\n"
3✔
219
        + "Reboot or open a new terminal to make it work.");
220
  }
1✔
221

222
  private void installIdeasyWindowsEnv(Path ideRoot, Path installationPath) {
223
    if (!this.context.getSystemInfo().isWindows()) {
5✔
224
      return;
1✔
225
    }
226
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
227
    helper.setUserEnvironmentValue(IdeVariables.IDE_ROOT.getName(), ideRoot.toString());
6✔
228
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
229
    if (userPath == null) {
2!
230
      LOG.error("Could not read user PATH from registry!");
×
231
    } else {
232
      LOG.info("Found user PATH={}", userPath);
4✔
233
      Path ideasyBinPath = installationPath.resolve("bin");
4✔
234
      SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
235
      if (path.getEntries().isEmpty()) {
4!
236
        LOG.warn("ATTENTION:\n"
×
237
            + "Your user specific PATH variable seems to be empty.\n"
238
            + "You can double check this by pressing [Windows][r] and launch the program SystemPropertiesAdvanced.\n"
239
            + "Then click on 'Environment variables' and check if 'PATH' is set in the 'user variables' from the upper list.\n"
240
            + "In case 'PATH' is defined there non-empty and you get this message, please abort and give us feedback:\n"
241
            + "https://github.com/devonfw/IDEasy/issues\n"
242
            + "Otherwise all is correct and you can continue.");
243
        this.context.askToContinue("Are you sure you want to override your PATH?");
×
244
      } else {
245
        path.removeEntries(s -> s.endsWith(IDE_INSTALLATION_BIN));
7✔
246
      }
247
      path.getEntries().add(ideasyBinPath.toString());
6✔
248
      helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), path.toString());
6✔
249
      setGitLongpaths();
2✔
250
    }
251
  }
1✔
252

253
  private void setGitLongpaths() {
254
    this.context.getGitContext().findGitRequired();
5✔
255
    Path configPath = this.context.getUserHome().resolve(".gitconfig");
6✔
256
    FileAccess fileAccess = this.context.getFileAccess();
4✔
257
    IniFile iniFile = fileAccess.readIniFile(configPath);
4✔
258
    IniSection coreSection = iniFile.getOrCreateSection("core");
4✔
259
    coreSection.setProperty("longpaths", "true");
4✔
260
    fileAccess.writeIniFile(iniFile, configPath);
4✔
261
  }
1✔
262

263
  /**
264
   * Sets up Windows Terminal with Git Bash integration.
265
   */
266
  public void setupWindowsTerminal() {
267
    if (!this.context.getSystemInfo().isWindows()) {
×
268
      return;
×
269
    }
270
    if (!isWindowsTerminalInstalled()) {
×
271
      try {
272
        installWindowsTerminal();
×
273
      } catch (Exception e) {
×
274
        LOG.error("Failed to install Windows Terminal!", e);
×
275
      }
×
276
    }
277
    configureWindowsTerminalGitBash();
×
278
  }
×
279

280
  /**
281
   * Checks if Windows Terminal is installed.
282
   *
283
   * @return {@code true} if Windows Terminal is installed, {@code false} otherwise.
284
   */
285
  private boolean isWindowsTerminalInstalled() {
286
    try {
287
      ProcessResult result = this.context.newProcess()
×
288
          .executable("powershell")
×
289
          .addArgs("-Command", "Get-AppxPackage -Name Microsoft.WindowsTerminal")
×
290
          .run(ProcessMode.DEFAULT_CAPTURE);
×
291
      return result.isSuccessful() && !result.getOut().isEmpty();
×
292
    } catch (Exception e) {
×
293
      LOG.debug("Failed to check Windows Terminal installation.", e);
×
294
      return false;
×
295
    }
296
  }
297

298
  /**
299
   * Installs Windows Terminal using winget.
300
   */
301
  private void installWindowsTerminal() {
302
    try {
303
      LOG.info("Installing Windows Terminal...");
×
304
      ProcessResult result = this.context.newProcess()
×
305
          .executable("winget")
×
306
          .addArgs("install", "Microsoft.WindowsTerminal")
×
307
          .run(ProcessMode.DEFAULT);
×
308
      if (result.isSuccessful()) {
×
309
        LOG.info(IdeLogLevel.SUCCESS.getSlf4jMarker(), "Windows Terminal has been installed successfully.");
×
310
      } else {
311
        LOG.warn("Failed to install Windows Terminal. Please install it manually from Microsoft Store.");
×
312
      }
313
    } catch (Exception e) {
×
314
      LOG.warn("Failed to install Windows Terminal: {}. Please install it manually from Microsoft Store.", e.toString());
×
315
    }
×
316
  }
×
317

318
  /**
319
   * Configures Git Bash integration in Windows Terminal.
320
   */
321
  protected void configureWindowsTerminalGitBash() {
322
    Path settingsPath = getWindowsTerminalSettingsPath();
3✔
323
    if (settingsPath == null || !Files.exists(settingsPath)) {
7!
324
      LOG.warn("Windows Terminal settings file not found. Cannot configure Git Bash integration.");
×
325
      return;
×
326
    }
327

328
    try {
329
      Path bashPath = this.context.findBash();
4✔
330
      if (bashPath == null) {
2!
331
        LOG.warn("Git Bash not found. Cannot configure Windows Terminal integration.");
×
332
        return;
×
333
      }
334

335
      configureGitBashProfile(settingsPath, bashPath.toString());
5✔
336
      LOG.info(IdeLogLevel.SUCCESS.getSlf4jMarker(), "Git Bash has been configured in Windows Terminal.");
5✔
337
    } catch (Exception e) {
×
338
      LOG.warn("Failed to configure Git Bash in Windows Terminal: {}", e.getMessage());
×
339
    }
1✔
340
  }
1✔
341

342
  /**
343
   * Gets the Windows Terminal settings file path.
344
   *
345
   * @return the {@link Path} to the Windows Terminal settings file, or {@code null} if not found.
346
   */
347
  protected Path getWindowsTerminalSettingsPath() {
348
    Path localAppData = this.context.getUserHome().resolve("AppData").resolve("Local");
8✔
349
    Path packagesPath = localAppData.resolve("Packages");
4✔
350

351
    // Try the new Windows Terminal package first
352
    Path newTerminalPath = packagesPath.resolve("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
4✔
353
        .resolve("LocalState").resolve("settings.json");
4✔
354
    if (Files.exists(newTerminalPath)) {
5!
355
      return newTerminalPath;
2✔
356
    }
357

358
    // Try the old Windows Terminal Preview package
359
    Path previewPath = packagesPath.resolve("Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe")
×
360
        .resolve("LocalState").resolve("settings.json");
×
361
    if (Files.exists(previewPath)) {
×
362
      return previewPath;
×
363
    }
364

365
    return null;
×
366
  }
367

368
  /**
369
   * Configures Git Bash profile in Windows Terminal settings.
370
   *
371
   * @param settingsPath the {@link Path} to the Windows Terminal settings file.
372
   * @param bashPath the path to the Git Bash executable.
373
   */
374
  private void configureGitBashProfile(Path settingsPath, String bashPath) throws Exception {
375

376
    ObjectMapper mapper = new ObjectMapper();
4✔
377
    ObjectNode root;
378
    try (Reader reader = Files.newBufferedReader(settingsPath)) {
3✔
379
      JsonNode rootNode = mapper.readTree(reader);
4✔
380
      root = (ObjectNode) rootNode;
3✔
381
    }
382

383
    // Get or create profiles object
384
    ObjectNode profiles = (ObjectNode) root.get("profiles");
5✔
385
    if (profiles == null) {
2!
386
      profiles = mapper.createObjectNode();
×
387
      root.set("profiles", profiles);
×
388
    }
389

390
    // Get or create list array of profiles object
391
    JsonNode profilesList = profiles.get("list");
4✔
392
    if (profilesList == null || !profilesList.isArray()) {
5!
393
      profilesList = mapper.createArrayNode();
×
394
      profiles.set("list", profilesList);
×
395
    }
396

397
    // Check if Git Bash profile already exists
398
    boolean gitBashProfileExists = false;
2✔
399
    for (JsonNode profile : profilesList) {
10✔
400
      if (profile.has("name") && profile.get("name").asText().equals("Git Bash")) {
11!
401
        gitBashProfileExists = true;
×
402
        break;
×
403
      }
404
    }
1✔
405

406
    // Add Git Bash profile if it doesn't exist
407
    if (gitBashProfileExists) {
2!
408
      LOG.info("Git Bash profile already exists in {}.", settingsPath);
×
409
    } else {
410
      ObjectNode gitBashProfile = mapper.createObjectNode();
3✔
411
      String newGuid = "{2ece5bfe-50ed-5f3a-ab87-5cd4baafed2b}";
2✔
412
      String iconPath = getGitBashIconPath(bashPath);
4✔
413
      String startingDirectory = this.context.getIdeRoot().toString();
5✔
414

415
      gitBashProfile.put("guid", newGuid);
5✔
416
      gitBashProfile.put("name", "Git Bash");
5✔
417
      gitBashProfile.put("commandline", bashPath);
5✔
418
      gitBashProfile.put("icon", iconPath);
5✔
419
      gitBashProfile.put("startingDirectory", startingDirectory);
5✔
420

421
      ((ArrayNode) profilesList).add(gitBashProfile);
5✔
422

423
      // Set Git Bash as default profile
424
      root.put("defaultProfile", newGuid);
5✔
425

426
      // Write back to file
427
      try (Writer writer = Files.newBufferedWriter(settingsPath)) {
5✔
428
        mapper.writerWithDefaultPrettyPrinter().writeValue(writer, root);
5✔
429
      }
430
    }
431
  }
1✔
432

433
  private String getGitBashIconPath(String bashPathString) {
434
    Path bashPath = Path.of(bashPathString);
5✔
435
    // "C:\\Program Files\\Git\\bin\\bash.exe"
436
    // "C:\\Program Files\\Git\\mingw64\\share\\git\\git-for-windows.ico"
437
    Path parent = bashPath.getParent();
3✔
438
    if (parent != null) {
2!
439
      Path iconPath = parent.resolve("mingw64/share/git/git-for-windows.ico");
4✔
440
      if (Files.exists(iconPath)) {
5!
441
        LOG.debug("Found git-bash icon at {}", iconPath);
×
442
        return iconPath.toString();
×
443
      }
444
      LOG.debug("Git Bash icon not found at {}. Using default icon.", iconPath);
4✔
445
    }
446
    return "ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png";
2✔
447
  }
448

449
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
450
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
451
  }
452

453
  static String removeEntryFromWindowsPath(String userPath, String suffix) {
454
    int len = userPath.length();
3✔
455
    int start = 0;
2✔
456
    while ((start >= 0) && (start < len)) {
5!
457
      int end = userPath.indexOf(';', start);
5✔
458
      if (end < 0) {
2✔
459
        end = len;
2✔
460
      }
461
      String entry = userPath.substring(start, end);
5✔
462
      if (entry.endsWith(suffix)) {
4✔
463
        String prefix = "";
2✔
464
        int offset = 1;
2✔
465
        if (start > 0) {
2✔
466
          prefix = userPath.substring(0, start - 1);
7✔
467
          offset = 0;
2✔
468
        }
469
        if (end == len) {
3✔
470
          return prefix;
2✔
471
        } else {
472
          return removeEntryFromWindowsPath(prefix + userPath.substring(end + offset), suffix);
10✔
473
        }
474
      }
475
      start = end + 1;
4✔
476
    }
1✔
477
    return userPath;
2✔
478
  }
479

480
  /**
481
   * Adds ourselves to the shell RC (run-commands) configuration file.
482
   *
483
   * @param filename the name of the RC file.
484
   * @param ideRoot the IDE_ROOT {@link Path}.
485
   */
486
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
487

488
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
489
  }
1✔
490

491
  private void removeFromShellRc(String filename, Path ideRoot) {
492

493
    modifyShellRc(filename, ideRoot, false, null);
6✔
494
  }
1✔
495

496
  /**
497
   * Adds ourselves to the shell RC (run-commands) configuration file.
498
   *
499
   * @param filename the name of the RC file.
500
   * @param ideRoot the IDE_ROOT {@link Path}.
501
   */
502
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
503

504
    if (add) {
2✔
505
      LOG.info("Configuring IDEasy in {}", filename);
5✔
506
    } else {
507
      LOG.info("Removing IDEasy from {}", filename);
4✔
508
    }
509
    Path rcFile = this.context.getUserHome().resolve(filename);
6✔
510
    FileAccess fileAccess = this.context.getFileAccess();
4✔
511
    List<String> lines = fileAccess.readFileLines(rcFile);
4✔
512
    if (lines == null) {
2✔
513
      if (!add) {
2!
514
        return;
×
515
      }
516
      lines = new ArrayList<>();
5✔
517
    } else {
518
      // since it is unspecified if the returned List may be immutable we want to get sure
519
      lines = new ArrayList<>(lines);
5✔
520
    }
521
    Iterator<String> iterator = lines.iterator();
3✔
522
    int removeCount = 0;
2✔
523
    while (iterator.hasNext()) {
3✔
524
      String line = iterator.next();
4✔
525
      line = line.trim();
3✔
526
      if (isObsoleteRcLine(line)) {
3✔
527
        LOG.info("Removing obsolete line from {}: {}", filename, line);
5✔
528
        iterator.remove();
2✔
529
        removeCount++;
2✔
530
      } else if (line.equals(extraLine)) {
4✔
531
        extraLine = null;
2✔
532
      }
533
    }
1✔
534
    if (add) {
2✔
535
      if (extraLine != null) {
2!
536
        lines.add(extraLine);
×
537
      }
538
      if (!this.context.getSystemInfo().isWindows()) {
5✔
539
        lines.add("export IDE_ROOT=\"" + WindowsPathSyntax.MSYS.format(ideRoot) + "\"");
7✔
540
      }
541
      lines.add(BASH_CODE_SOURCE_FUNCTIONS);
4✔
542
    }
543
    fileAccess.writeFileLines(lines, rcFile);
4✔
544
    LOG.debug("Successfully updated {}", filename);
4✔
545
  }
1✔
546

547
  private static boolean isObsoleteRcLine(String line) {
548
    if (line.startsWith("alias ide=")) {
4!
549
      return true;
×
550
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
551
      return true;
×
552
    } else if (line.equals("ide")) {
4✔
553
      return true;
2✔
554
    } else if (line.equals("ide init")) {
4✔
555
      return true;
2✔
556
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
557
      return true;
2✔
558
    }
559
    return false;
2✔
560
  }
561

562
  private boolean addInstallationArtifact(Path cwd, String artifactName, boolean required, List<Path> installationArtifacts) {
563

564
    Path artifactPath = cwd.resolve(artifactName);
4✔
565
    if (Files.exists(artifactPath)) {
5!
566
      installationArtifacts.add(artifactPath);
5✔
567
    } else if (required) {
×
568
      LOG.error("Missing required file {}", artifactName);
×
569
      return false;
×
570
    }
571
    return true;
2✔
572
  }
573

574
  private Path determineIdeRoot(Path cwd) {
575
    Path ideRoot = this.context.getIdeRoot();
4✔
576
    if (ideRoot == null) {
2!
577
      Path home = this.context.getUserHome();
4✔
578
      Path installRoot = home;
2✔
579
      if (this.context.getSystemInfo().isWindows()) {
5✔
580
        if (!cwd.startsWith(home)) {
4!
581
          installRoot = cwd.getRoot();
×
582
        }
583
      }
584
      ideRoot = installRoot.resolve(IdeContext.FOLDER_PROJECTS);
4✔
585
    } else {
1✔
586
      assert (Files.isDirectory(ideRoot)) : "IDE_ROOT directory does not exist!";
×
587
    }
588
    return ideRoot;
2✔
589
  }
590

591
  /**
592
   * Uninstalls IDEasy entirely from the system.
593
   */
594
  public void uninstallIdeasy() {
595

596
    Path ideRoot = this.context.getIdeRoot();
4✔
597
    removeFromShellRc(BASHRC, ideRoot);
4✔
598
    removeFromShellRc(ZSHRC, ideRoot);
4✔
599
    Path idePath = this.context.getIdePath();
4✔
600
    uninstallIdeasyWindowsEnv(ideRoot);
3✔
601
    uninstallIdeasyIdePath(idePath);
3✔
602
    deleteDownloadCache();
2✔
603
    LOG.info(IdeLogLevel.SUCCESS.getSlf4jMarker(), "IDEasy has been uninstalled from your system.");
5✔
604
    LOG.info(IdeLogLevel.INTERACTION.getSlf4jMarker(), "ATTENTION:\n"
6✔
605
        + "In order to prevent data-loss, we do not delete your projects and git repositories!\n"
606
        + "To entirely get rid of IDEasy, also check your IDE_ROOT folder at:\n"
607
        + "{}", ideRoot);
608
  }
1✔
609

610
  private void deleteDownloadCache() {
611
    Path downloadPath = this.context.getDownloadPath();
4✔
612
    LOG.info("Deleting download cache from {}", downloadPath);
4✔
613
    this.context.getFileAccess().delete(downloadPath);
5✔
614
  }
1✔
615

616
  private void uninstallIdeasyIdePath(Path idePath) {
617
    if (this.context.getSystemInfo().isWindows()) {
5✔
618
      this.context.newProcess().executable("bash").addArgs("-c",
17✔
619
          "sleep 10 && rm -rf \"" + WindowsPathSyntax.MSYS.format(idePath) + "\"").run(ProcessMode.BACKGROUND);
5✔
620
      LOG.info(IdeLogLevel.INTERACTION.getSlf4jMarker(),
7✔
621
          "To prevent windows file locking errors, we perform an asynchronous deletion of {} in background now.\n"
622
              + "Please close all terminals and wait a minute for the deletion to complete before running other commands.", idePath);
623
    } else {
624
      LOG.info("Finally deleting {}", idePath);
4✔
625
      this.context.getFileAccess().delete(idePath);
5✔
626
    }
627
  }
1✔
628

629
  private void uninstallIdeasyWindowsEnv(Path ideRoot) {
630
    if (!this.context.getSystemInfo().isWindows()) {
5✔
631
      return;
1✔
632
    }
633
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
634
    helper.removeUserEnvironmentValue(IdeVariables.IDE_ROOT.getName());
4✔
635
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
636
    if (userPath == null) {
2!
637
      LOG.error("Could not read user PATH from registry!");
×
638
    } else {
639
      LOG.info("Found user PATH={}", userPath);
4✔
640
      String newUserPath = userPath;
2✔
641
      if (!userPath.isEmpty()) {
3!
642
        SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
643
        path.removeEntries(s -> s.endsWith(IDE_BIN) || s.endsWith(IDE_INSTALLATION_BIN));
15!
644
        newUserPath = path.toString();
3✔
645
      }
646
      if (newUserPath.equals(userPath)) {
4!
647
        LOG.error("Could not find IDEasy in PATH:\n{}", userPath);
×
648
      } else {
649
        helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), newUserPath);
5✔
650
      }
651
    }
652
  }
1✔
653
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc