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

devonfw / IDEasy / 19639829847

24 Nov 2025 03:32PM UTC coverage: 69.124% (+0.2%) from 68.924%
19639829847

Pull #1593

github

web-flow
Merge 42115d8be into ffcb5d97f
Pull Request #1593: #1144: #1145: CVE warnings and suggestions

3598 of 5699 branches covered (63.13%)

Branch coverage included in aggregate %.

9358 of 13044 relevant lines covered (71.74%)

3.15 hits per line

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

69.6
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.git.GitContext;
18
import com.devonfw.tools.ide.git.GitContextImpl;
19
import com.devonfw.tools.ide.io.FileAccess;
20
import com.devonfw.tools.ide.io.ini.IniFile;
21
import com.devonfw.tools.ide.io.ini.IniSection;
22
import com.devonfw.tools.ide.os.WindowsHelper;
23
import com.devonfw.tools.ide.os.WindowsPathSyntax;
24
import com.devonfw.tools.ide.process.ProcessMode;
25
import com.devonfw.tools.ide.process.ProcessResult;
26
import com.devonfw.tools.ide.tool.mvn.MvnArtifact;
27
import com.devonfw.tools.ide.tool.mvn.MvnBasedLocalToolCommandlet;
28
import com.devonfw.tools.ide.tool.repository.MvnRepository;
29
import com.devonfw.tools.ide.variable.IdeVariables;
30
import com.devonfw.tools.ide.version.IdeVersion;
31
import com.devonfw.tools.ide.version.VersionIdentifier;
32
import com.fasterxml.jackson.databind.JsonNode;
33
import com.fasterxml.jackson.databind.ObjectMapper;
34
import com.fasterxml.jackson.databind.node.ArrayNode;
35
import com.fasterxml.jackson.databind.node.ObjectNode;
36

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

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

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

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

54
  private final UpgradeMode mode;
55

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

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

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

77
  @Override
78
  public VersionIdentifier getInstalledVersion() {
79

80
    return IdeVersion.getVersionIdentifier();
2✔
81
  }
82

83
  @Override
84
  public String getConfiguredEdition() {
85

86
    return this.tool;
3✔
87
  }
88

89
  @Override
90
  public VersionIdentifier getConfiguredVersion() {
91

92
    UpgradeMode upgradeMode = this.mode;
×
93
    if (upgradeMode == null) {
×
94
      if (IdeVersion.isSnapshot()) {
×
95
        upgradeMode = UpgradeMode.SNAPSHOT;
×
96
      } else {
97
        if (IdeVersion.getVersionIdentifier().getDevelopmentPhase().isStable()) {
×
98
          upgradeMode = UpgradeMode.STABLE;
×
99
        } else {
100
          upgradeMode = UpgradeMode.UNSTABLE;
×
101
        }
102
      }
103
    }
104
    return upgradeMode.getVersion();
×
105
  }
106

107
  @Override
108
  public Path getToolPath() {
109

110
    return this.context.getIdeInstallationPath();
×
111
  }
112

113
  @Override
114
  public ToolInstallation install(boolean silent) {
115

116
    this.context.requireOnline("upgrade of IDEasy", true);
5✔
117

118
    if (IdeVersion.isUndefined() && !this.context.isForceMode()) {
6!
119
      VersionIdentifier version = IdeVersion.getVersionIdentifier();
2✔
120
      this.context.warning("You are using IDEasy version {} which indicates local development - skipping upgrade.", version);
10✔
121
      return toolAlreadyInstalled(silent, getToolWithEdition(), version, this.context.newProcess(), false);
11✔
122
    }
123
    return super.install(silent);
×
124
  }
125

126
  /**
127
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
128
   */
129
  public VersionIdentifier getLatestVersion() {
130

131
    VersionIdentifier currentVersion = IdeVersion.getVersionIdentifier();
2✔
132
    if (IdeVersion.isUndefined()) {
2!
133
      return currentVersion;
2✔
134
    }
135
    VersionIdentifier configuredVersion = getConfiguredVersion();
×
136
    return getToolRepository().resolveVersion(this.tool, getConfiguredEdition(), configuredVersion, this);
×
137
  }
138

139
  /**
140
   * Checks if an update is available and logs according information.
141
   *
142
   * @return {@code true} if an update is available, {@code false} otherwise.
143
   */
144
  public boolean checkIfUpdateIsAvailable() {
145
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
146
    this.context.success("Your version of IDEasy is {}.", installedVersion);
10✔
147
    if (IdeVersion.isSnapshot()) {
2!
148
      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✔
149
    }
150
    if (this.context.isOffline()) {
4✔
151
      this.context.warning("Skipping check for newer version of IDEasy because you are offline.");
4✔
152
      return false;
2✔
153
    }
154
    VersionIdentifier latestVersion = getLatestVersion();
3✔
155
    if (installedVersion.equals(latestVersion)) {
4!
156
      this.context.success("Your are using the latest version of IDEasy and no update is available.");
4✔
157
      return false;
2✔
158
    } else {
159
      this.context.interaction("Your version of IDEasy is {} but version {} is available. Please run the following command to upgrade to the latest version:\n"
×
160
          + "ide upgrade", installedVersion, latestVersion);
161
      return true;
×
162
    }
163
  }
164

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

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

239
  private void setGitLongpaths() {
240
    GitContext gitContext = new GitContextImpl(this.context);
6✔
241
    gitContext.verifyGitInstalled();
2✔
242
    Path configPath = this.context.getUserHome().resolve(".gitconfig");
6✔
243
    FileAccess fileAccess = this.context.getFileAccess();
4✔
244
    IniFile iniFile = fileAccess.readIniFile(configPath);
4✔
245
    IniSection coreSection = iniFile.getOrCreateSection("core");
4✔
246
    coreSection.setProperty("longpaths", "true");
4✔
247
    fileAccess.writeIniFile(iniFile, configPath);
4✔
248
  }
1✔
249

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

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

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

305
  /**
306
   * Configures Git Bash integration in Windows Terminal.
307
   */
308
  protected void configureWindowsTerminalGitBash() {
309
    Path settingsPath = getWindowsTerminalSettingsPath();
3✔
310
    if (settingsPath == null || !Files.exists(settingsPath)) {
7!
311
      this.context.warning("Windows Terminal settings file not found. Cannot configure Git Bash integration.");
×
312
      return;
×
313
    }
314

315
    try {
316
      String bashPath = this.context.findBash();
4✔
317
      if (bashPath == null) {
2!
318
        this.context.warning("Git Bash not found. Cannot configure Windows Terminal integration.");
×
319
        return;
×
320
      }
321

322
      configureGitBashProfile(settingsPath, bashPath);
4✔
323
      this.context.success("Git Bash has been configured in Windows Terminal.");
4✔
324
    } catch (Exception e) {
×
325
      this.context.warning("Failed to configure Git Bash in Windows Terminal: {}", e.getMessage());
×
326
    }
1✔
327
  }
1✔
328

329
  /**
330
   * Gets the Windows Terminal settings file path.
331
   *
332
   * @return the {@link Path} to the Windows Terminal settings file, or {@code null} if not found.
333
   */
334
  protected Path getWindowsTerminalSettingsPath() {
335
    Path localAppData = this.context.getUserHome().resolve("AppData").resolve("Local");
8✔
336
    Path packagesPath = localAppData.resolve("Packages");
4✔
337

338
    // Try the new Windows Terminal package first
339
    Path newTerminalPath = packagesPath.resolve("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
4✔
340
        .resolve("LocalState").resolve("settings.json");
4✔
341
    if (Files.exists(newTerminalPath)) {
5!
342
      return newTerminalPath;
2✔
343
    }
344

345
    // Try the old Windows Terminal Preview package
346
    Path previewPath = packagesPath.resolve("Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe")
×
347
        .resolve("LocalState").resolve("settings.json");
×
348
    if (Files.exists(previewPath)) {
×
349
      return previewPath;
×
350
    }
351

352
    return null;
×
353
  }
354

355
  /**
356
   * Configures Git Bash profile in Windows Terminal settings.
357
   *
358
   * @param settingsPath the {@link Path} to the Windows Terminal settings file.
359
   * @param bashPath the path to the Git Bash executable.
360
   */
361
  private void configureGitBashProfile(Path settingsPath, String bashPath) throws Exception {
362

363
    ObjectMapper mapper = new ObjectMapper();
4✔
364
    ObjectNode root;
365
    try (Reader reader = Files.newBufferedReader(settingsPath)) {
3✔
366
      JsonNode rootNode = mapper.readTree(reader);
4✔
367
      root = (ObjectNode) rootNode;
3✔
368
    }
369

370
    // Get or create profiles object
371
    ObjectNode profiles = (ObjectNode) root.get("profiles");
5✔
372
    if (profiles == null) {
2!
373
      profiles = mapper.createObjectNode();
×
374
      root.set("profiles", profiles);
×
375
    }
376

377
    // Get or create list array of profiles object
378
    JsonNode profilesList = profiles.get("list");
4✔
379
    if (profilesList == null || !profilesList.isArray()) {
5!
380
      profilesList = mapper.createArrayNode();
×
381
      profiles.set("list", profilesList);
×
382
    }
383

384
    // Check if Git Bash profile already exists
385
    boolean gitBashProfileExists = false;
2✔
386
    for (JsonNode profile : profilesList) {
10✔
387
      if (profile.has("name") && profile.get("name").asText().equals("Git Bash")) {
11!
388
        gitBashProfileExists = true;
×
389
        break;
×
390
      }
391
    }
1✔
392

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

402
      gitBashProfile.put("guid", newGuid);
5✔
403
      gitBashProfile.put("name", "Git Bash");
5✔
404
      gitBashProfile.put("commandline", bashPath);
5✔
405
      gitBashProfile.put("icon", iconPath);
5✔
406
      gitBashProfile.put("startingDirectory", startingDirectory);
5✔
407

408
      ((ArrayNode) profilesList).add(gitBashProfile);
5✔
409

410
      // Set Git Bash as default profile
411
      root.put("defaultProfile", newGuid);
5✔
412

413
      // Write back to file
414
      try (Writer writer = Files.newBufferedWriter(settingsPath)) {
5✔
415
        mapper.writerWithDefaultPrettyPrinter().writeValue(writer, root);
5✔
416
      }
417
    }
418
  }
1✔
419

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

436
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
437
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
438
  }
439

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

467
  /**
468
   * Adds ourselves to the shell RC (run-commands) configuration file.
469
   *
470
   * @param filename the name of the RC file.
471
   * @param ideRoot the IDE_ROOT {@link Path}.
472
   */
473
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
474

475
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
476
  }
1✔
477

478
  private void removeFromShellRc(String filename, Path ideRoot) {
479

480
    modifyShellRc(filename, ideRoot, false, null);
6✔
481
  }
1✔
482

483
  /**
484
   * Adds ourselves to the shell RC (run-commands) configuration file.
485
   *
486
   * @param filename the name of the RC file.
487
   * @param ideRoot the IDE_ROOT {@link Path}.
488
   */
489
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
490

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

534
  private static boolean isObsoleteRcLine(String line) {
535
    if (line.startsWith("alias ide=")) {
4!
536
      return true;
×
537
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
538
      return true;
×
539
    } else if (line.equals("ide")) {
4✔
540
      return true;
2✔
541
    } else if (line.equals("ide init")) {
4✔
542
      return true;
2✔
543
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
544
      return true;
2✔
545
    }
546
    return false;
2✔
547
  }
548

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

551
    Path artifactPath = cwd.resolve(artifactName);
4✔
552
    if (Files.exists(artifactPath)) {
5!
553
      installationArtifacts.add(artifactPath);
5✔
554
    } else if (required) {
×
555
      this.context.error("Missing required file {}", artifactName);
×
556
      return false;
×
557
    }
558
    return true;
2✔
559
  }
560

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

578
  /**
579
   * Uninstalls IDEasy entirely from the system.
580
   */
581
  public void uninstallIdeasy() {
582

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

597
  private void deleteDownloadCache() {
598
    Path downloadPath = this.context.getDownloadPath();
4✔
599
    this.context.info("Deleting download cache from {}", downloadPath);
10✔
600
    this.context.getFileAccess().delete(downloadPath);
5✔
601
  }
1✔
602

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

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