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

devonfw / IDEasy / 21177134407

20 Jan 2026 03:26PM UTC coverage: 70.457% (+0.01%) from 70.447%
21177134407

Pull #1682

github

web-flow
Merge ade595c93 into f198cfff3
Pull Request #1682: #1673: log error instead of throwing if installation version already present

4032 of 6308 branches covered (63.92%)

Branch coverage included in aggregate %.

10480 of 14289 relevant lines covered (73.34%)

3.18 hits per line

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

72.21
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 com.devonfw.tools.ide.cli.CliException;
13
import com.devonfw.tools.ide.commandlet.UpgradeMode;
14
import com.devonfw.tools.ide.common.SimpleSystemPath;
15
import com.devonfw.tools.ide.common.Tag;
16
import com.devonfw.tools.ide.context.IdeContext;
17
import com.devonfw.tools.ide.io.FileAccess;
18
import com.devonfw.tools.ide.io.ini.IniFile;
19
import com.devonfw.tools.ide.io.ini.IniSection;
20
import com.devonfw.tools.ide.os.WindowsHelper;
21
import com.devonfw.tools.ide.os.WindowsPathSyntax;
22
import com.devonfw.tools.ide.process.ProcessMode;
23
import com.devonfw.tools.ide.process.ProcessResult;
24
import com.devonfw.tools.ide.tool.mvn.MvnArtifact;
25
import com.devonfw.tools.ide.tool.mvn.MvnBasedLocalToolCommandlet;
26
import com.devonfw.tools.ide.tool.mvn.MvnRepository;
27
import com.devonfw.tools.ide.variable.IdeVariables;
28
import com.devonfw.tools.ide.version.IdeVersion;
29
import com.devonfw.tools.ide.version.VersionIdentifier;
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.fasterxml.jackson.databind.ObjectMapper;
32
import com.fasterxml.jackson.databind.node.ArrayNode;
33
import com.fasterxml.jackson.databind.node.ObjectNode;
34

35
/**
36
 * {@link MvnBasedLocalToolCommandlet} for IDEasy (ide-cli).
37
 */
38
public class IdeasyCommandlet extends MvnBasedLocalToolCommandlet {
39

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

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

45
  /** The {@link #getName() tool name}. */
46
  public static final String TOOL_NAME = "ideasy";
47
  public static final String BASHRC = ".bashrc";
48
  public static final String ZSHRC = ".zshrc";
49
  public static final String IDE_BIN = "\\_ide\\bin";
50
  public static final String IDE_INSTALLATION_BIN = "\\_ide\\installation\\bin";
51

52
  private final UpgradeMode mode;
53

54
  /**
55
   * The constructor.
56
   *
57
   * @param context the {@link IdeContext}.
58
   */
59
  public IdeasyCommandlet(IdeContext context) {
60
    this(context, UpgradeMode.STABLE);
4✔
61
  }
1✔
62

63
  /**
64
   * The constructor.
65
   *
66
   * @param context the {@link IdeContext}.
67
   * @param mode the {@link UpgradeMode}.
68
   */
69
  public IdeasyCommandlet(IdeContext context, UpgradeMode mode) {
70

71
    super(context, TOOL_NAME, ARTIFACT, Set.of(Tag.PRODUCTIVITY, Tag.IDE));
8✔
72
    this.mode = mode;
3✔
73
  }
1✔
74

75
  @Override
76
  public VersionIdentifier getInstalledVersion() {
77

78
    return IdeVersion.getVersionIdentifier();
2✔
79
  }
80

81
  @Override
82
  public String getInstalledEdition() {
83

84
    return this.tool;
3✔
85
  }
86

87
  @Override
88
  public String getConfiguredEdition() {
89

90
    return this.tool;
3✔
91
  }
92

93
  @Override
94
  public VersionIdentifier getConfiguredVersion() {
95

96
    UpgradeMode upgradeMode = this.mode;
3✔
97
    if (upgradeMode == null) {
2!
98
      if (IdeVersion.isSnapshot()) {
2!
99
        upgradeMode = UpgradeMode.SNAPSHOT;
3✔
100
      } else {
101
        if (IdeVersion.getVersionIdentifier().getDevelopmentPhase().isStable()) {
×
102
          upgradeMode = UpgradeMode.STABLE;
×
103
        } else {
104
          upgradeMode = UpgradeMode.UNSTABLE;
×
105
        }
106
      }
107
    }
108
    return upgradeMode.getVersion();
3✔
109
  }
110

111
  @Override
112
  public Path getToolPath() {
113

114
    return this.context.getIdeInstallationPath();
4✔
115
  }
116

117
  @Override
118
  protected ToolInstallation doInstall(ToolInstallRequest request) {
119

120
    this.context.requireOnline("upgrade of IDEasy", true);
5✔
121

122
    if (IdeVersion.isUndefined() && !this.context.isForceMode()) {
6!
123
      VersionIdentifier version = IdeVersion.getVersionIdentifier();
2✔
124
      this.context.warning("You are using IDEasy version {} which indicates local development - skipping upgrade.", version);
10✔
125
      return toolAlreadyInstalled(request);
4✔
126
    }
127
    return super.doInstall(request);
×
128
  }
129

130
  /**
131
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
132
   */
133
  public VersionIdentifier getLatestVersion() {
134

135
    if (!this.context.isForceMode()) {
4!
136
      VersionIdentifier currentVersion = IdeVersion.getVersionIdentifier();
2✔
137
      if (IdeVersion.isUndefined()) {
2!
138
        return currentVersion;
2✔
139
      }
140
    }
141
    VersionIdentifier configuredVersion = getConfiguredVersion();
×
142
    return getToolRepository().resolveVersion(this.tool, getConfiguredEdition(), configuredVersion, this);
×
143
  }
144

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

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

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

246
  private void setGitLongpaths() {
247
    this.context.getGitContext().findGitRequired();
5✔
248
    Path configPath = this.context.getUserHome().resolve(".gitconfig");
6✔
249
    FileAccess fileAccess = this.context.getFileAccess();
4✔
250
    IniFile iniFile = fileAccess.readIniFile(configPath);
4✔
251
    IniSection coreSection = iniFile.getOrCreateSection("core");
4✔
252
    coreSection.setProperty("longpaths", "true");
4✔
253
    fileAccess.writeIniFile(iniFile, configPath);
4✔
254
  }
1✔
255

256
  /**
257
   * Sets up Windows Terminal with Git Bash integration.
258
   */
259
  public void setupWindowsTerminal() {
260
    if (!this.context.getSystemInfo().isWindows()) {
×
261
      return;
×
262
    }
263
    if (!isWindowsTerminalInstalled()) {
×
264
      try {
265
        installWindowsTerminal();
×
266
      } catch (Exception e) {
×
267
        this.context.error(e, "Failed to install Windows Terminal!");
×
268
      }
×
269
    }
270
    configureWindowsTerminalGitBash();
×
271
  }
×
272

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

291
  /**
292
   * Installs Windows Terminal using winget.
293
   */
294
  private void installWindowsTerminal() {
295
    try {
296
      this.context.info("Installing Windows Terminal...");
×
297
      ProcessResult result = this.context.newProcess()
×
298
          .executable("winget")
×
299
          .addArgs("install", "Microsoft.WindowsTerminal")
×
300
          .run(ProcessMode.DEFAULT);
×
301
      if (result.isSuccessful()) {
×
302
        this.context.success("Windows Terminal has been installed successfully.");
×
303
      } else {
304
        this.context.warning("Failed to install Windows Terminal. Please install it manually from Microsoft Store.");
×
305
      }
306
    } catch (Exception e) {
×
307
      this.context.warning("Failed to install Windows Terminal: {}. Please install it manually from Microsoft Store.", e.getMessage());
×
308
    }
×
309
  }
×
310

311
  /**
312
   * Configures Git Bash integration in Windows Terminal.
313
   */
314
  protected void configureWindowsTerminalGitBash() {
315
    Path settingsPath = getWindowsTerminalSettingsPath();
3✔
316
    if (settingsPath == null || !Files.exists(settingsPath)) {
7!
317
      this.context.warning("Windows Terminal settings file not found. Cannot configure Git Bash integration.");
×
318
      return;
×
319
    }
320

321
    try {
322
      Path bashPath = this.context.findBash();
4✔
323
      if (bashPath == null) {
2!
324
        this.context.warning("Git Bash not found. Cannot configure Windows Terminal integration.");
×
325
        return;
×
326
      }
327

328
      configureGitBashProfile(settingsPath, bashPath.toString());
5✔
329
      this.context.success("Git Bash has been configured in Windows Terminal.");
4✔
330
    } catch (Exception e) {
×
331
      this.context.warning("Failed to configure Git Bash in Windows Terminal: {}", e.getMessage());
×
332
    }
1✔
333
  }
1✔
334

335
  /**
336
   * Gets the Windows Terminal settings file path.
337
   *
338
   * @return the {@link Path} to the Windows Terminal settings file, or {@code null} if not found.
339
   */
340
  protected Path getWindowsTerminalSettingsPath() {
341
    Path localAppData = this.context.getUserHome().resolve("AppData").resolve("Local");
8✔
342
    Path packagesPath = localAppData.resolve("Packages");
4✔
343

344
    // Try the new Windows Terminal package first
345
    Path newTerminalPath = packagesPath.resolve("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
4✔
346
        .resolve("LocalState").resolve("settings.json");
4✔
347
    if (Files.exists(newTerminalPath)) {
5!
348
      return newTerminalPath;
2✔
349
    }
350

351
    // Try the old Windows Terminal Preview package
352
    Path previewPath = packagesPath.resolve("Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe")
×
353
        .resolve("LocalState").resolve("settings.json");
×
354
    if (Files.exists(previewPath)) {
×
355
      return previewPath;
×
356
    }
357

358
    return null;
×
359
  }
360

361
  /**
362
   * Configures Git Bash profile in Windows Terminal settings.
363
   *
364
   * @param settingsPath the {@link Path} to the Windows Terminal settings file.
365
   * @param bashPath the path to the Git Bash executable.
366
   */
367
  private void configureGitBashProfile(Path settingsPath, String bashPath) throws Exception {
368

369
    ObjectMapper mapper = new ObjectMapper();
4✔
370
    ObjectNode root;
371
    try (Reader reader = Files.newBufferedReader(settingsPath)) {
3✔
372
      JsonNode rootNode = mapper.readTree(reader);
4✔
373
      root = (ObjectNode) rootNode;
3✔
374
    }
375

376
    // Get or create profiles object
377
    ObjectNode profiles = (ObjectNode) root.get("profiles");
5✔
378
    if (profiles == null) {
2!
379
      profiles = mapper.createObjectNode();
×
380
      root.set("profiles", profiles);
×
381
    }
382

383
    // Get or create list array of profiles object
384
    JsonNode profilesList = profiles.get("list");
4✔
385
    if (profilesList == null || !profilesList.isArray()) {
5!
386
      profilesList = mapper.createArrayNode();
×
387
      profiles.set("list", profilesList);
×
388
    }
389

390
    // Check if Git Bash profile already exists
391
    boolean gitBashProfileExists = false;
2✔
392
    for (JsonNode profile : profilesList) {
10✔
393
      if (profile.has("name") && profile.get("name").asText().equals("Git Bash")) {
11!
394
        gitBashProfileExists = true;
×
395
        break;
×
396
      }
397
    }
1✔
398

399
    // Add Git Bash profile if it doesn't exist
400
    if (gitBashProfileExists) {
2!
401
      this.context.info("Git Bash profile already exists in {}.", settingsPath);
×
402
    } else {
403
      ObjectNode gitBashProfile = mapper.createObjectNode();
3✔
404
      String newGuid = "{2ece5bfe-50ed-5f3a-ab87-5cd4baafed2b}";
2✔
405
      String iconPath = getGitBashIconPath(bashPath);
4✔
406
      String startingDirectory = this.context.getIdeRoot().toString();
5✔
407

408
      gitBashProfile.put("guid", newGuid);
5✔
409
      gitBashProfile.put("name", "Git Bash");
5✔
410
      gitBashProfile.put("commandline", bashPath);
5✔
411
      gitBashProfile.put("icon", iconPath);
5✔
412
      gitBashProfile.put("startingDirectory", startingDirectory);
5✔
413

414
      ((ArrayNode) profilesList).add(gitBashProfile);
5✔
415

416
      // Set Git Bash as default profile
417
      root.put("defaultProfile", newGuid);
5✔
418

419
      // Write back to file
420
      try (Writer writer = Files.newBufferedWriter(settingsPath)) {
5✔
421
        mapper.writerWithDefaultPrettyPrinter().writeValue(writer, root);
5✔
422
      }
423
    }
424
  }
1✔
425

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

442
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
443
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
444
  }
445

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

473
  /**
474
   * Adds ourselves to the shell RC (run-commands) configuration file.
475
   *
476
   * @param filename the name of the RC file.
477
   * @param ideRoot the IDE_ROOT {@link Path}.
478
   */
479
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
480

481
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
482
  }
1✔
483

484
  private void removeFromShellRc(String filename, Path ideRoot) {
485

486
    modifyShellRc(filename, ideRoot, false, null);
6✔
487
  }
1✔
488

489
  /**
490
   * Adds ourselves to the shell RC (run-commands) configuration file.
491
   *
492
   * @param filename the name of the RC file.
493
   * @param ideRoot the IDE_ROOT {@link Path}.
494
   */
495
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
496

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

540
  private static boolean isObsoleteRcLine(String line) {
541
    if (line.startsWith("alias ide=")) {
4!
542
      return true;
×
543
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
544
      return true;
×
545
    } else if (line.equals("ide")) {
4✔
546
      return true;
2✔
547
    } else if (line.equals("ide init")) {
4✔
548
      return true;
2✔
549
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
550
      return true;
2✔
551
    }
552
    return false;
2✔
553
  }
554

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

557
    Path artifactPath = cwd.resolve(artifactName);
4✔
558
    if (Files.exists(artifactPath)) {
5!
559
      installationArtifacts.add(artifactPath);
5✔
560
    } else if (required) {
×
561
      this.context.error("Missing required file {}", artifactName);
×
562
      return false;
×
563
    }
564
    return true;
2✔
565
  }
566

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

584
  /**
585
   * Uninstalls IDEasy entirely from the system.
586
   */
587
  public void uninstallIdeasy() {
588

589
    Path ideRoot = this.context.getIdeRoot();
4✔
590
    removeFromShellRc(BASHRC, ideRoot);
4✔
591
    removeFromShellRc(ZSHRC, ideRoot);
4✔
592
    Path idePath = this.context.getIdePath();
4✔
593
    uninstallIdeasyWindowsEnv(ideRoot);
3✔
594
    uninstallIdeasyIdePath(idePath);
3✔
595
    deleteDownloadCache();
2✔
596
    this.context.success("IDEasy has been uninstalled from your system.");
4✔
597
    this.context.interaction("ATTENTION:\n"
10✔
598
        + "In order to prevent data-loss, we do not delete your projects and git repositories!\n"
599
        + "To entirely get rid of IDEasy, also check your IDE_ROOT folder at:\n"
600
        + "{}", ideRoot);
601
  }
1✔
602

603
  private void deleteDownloadCache() {
604
    Path downloadPath = this.context.getDownloadPath();
4✔
605
    this.context.info("Deleting download cache from {}", downloadPath);
10✔
606
    this.context.getFileAccess().delete(downloadPath);
5✔
607
  }
1✔
608

609
  private void uninstallIdeasyIdePath(Path idePath) {
610
    if (this.context.getSystemInfo().isWindows()) {
5✔
611
      this.context.newProcess().executable("bash").addArgs("-c",
17✔
612
          "sleep 10 && rm -rf \"" + WindowsPathSyntax.MSYS.format(idePath) + "\"").run(ProcessMode.BACKGROUND);
5✔
613
      this.context.interaction("To prevent windows file locking errors, we perform an asynchronous deletion of {} in background now.\n"
11✔
614
          + "Please close all terminals and wait a minute for the deletion to complete before running other commands.", idePath);
615
    } else {
616
      this.context.info("Finally deleting {}", idePath);
10✔
617
      this.context.getFileAccess().delete(idePath);
5✔
618
    }
619
  }
1✔
620

621
  private void uninstallIdeasyWindowsEnv(Path ideRoot) {
622
    if (!this.context.getSystemInfo().isWindows()) {
5✔
623
      return;
1✔
624
    }
625
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
626
    helper.removeUserEnvironmentValue(IdeVariables.IDE_ROOT.getName());
4✔
627
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
628
    if (userPath == null) {
2!
629
      this.context.error("Could not read user PATH from registry!");
×
630
    } else {
631
      this.context.info("Found user PATH={}", userPath);
10✔
632
      String newUserPath = userPath;
2✔
633
      if (!userPath.isEmpty()) {
3!
634
        SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
635
        path.removeEntries(s -> s.endsWith(IDE_BIN) || s.endsWith(IDE_INSTALLATION_BIN));
15!
636
        newUserPath = path.toString();
3✔
637
      }
638
      if (newUserPath.equals(userPath)) {
4!
639
        this.context.error("Could not find IDEasy in PATH:\n{}", userPath);
×
640
      } else {
641
        helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), newUserPath);
5✔
642
      }
643
    }
644
  }
1✔
645
}
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