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

devonfw / IDEasy / 30615771109

31 Jul 2026 08:17AM UTC coverage: 72.829% (+0.2%) from 72.593%
30615771109

Pull #2230

github

web-flow
Merge 86f291a3d into 468118d46
Pull Request #2230: #1953: Complete cleanup commandlet implementation and review handoverShow more lines

5092 of 7729 branches covered (65.88%)

Branch coverage included in aggregate %.

13258 of 17467 relevant lines covered (75.9%)

3.22 hits per line

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

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

3
import java.io.IOException;
4
import java.io.Reader;
5
import java.io.Writer;
6
import java.nio.file.Files;
7
import java.nio.file.Path;
8
import java.util.ArrayList;
9
import java.util.Iterator;
10
import java.util.List;
11
import java.util.Map;
12
import java.util.Objects;
13
import java.util.Set;
14
import java.util.regex.Matcher;
15
import java.util.regex.Pattern;
16

17
import org.slf4j.Logger;
18
import org.slf4j.LoggerFactory;
19

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

44
/**
45
 * {@link MvnBasedLocalToolCommandlet} for IDEasy (ide-cli).
46
 */
47
public class IdeasyCommandlet extends MvnBasedLocalToolCommandlet {
48

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

51
  /** The {@link MvnArtifact} for IDEasy. */
52
  public static final MvnArtifact ARTIFACT = MvnArtifact.ofIdeasyCli("*!", "tar.gz", "${os}-${arch}");
5✔
53

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

56
  /** The {@link #getName() tool name}. */
57
  public static final String TOOL_NAME = "ideasy";
58
  public static final String BASHRC = ".bashrc";
59
  public static final String ZSHRC = ".zshrc";
60
  public static final String IDE_BIN = "\\_ide\\bin";
61
  public static final String IDE_INSTALLATION_BIN = "\\_ide\\installation\\bin";
62

63

64
  private static final Map<String, Boolean> REQUIRED_INSTALLATION_ARTIFACTS = Map.of(
4✔
65
      //artifactName: String, required: boolean
66
      "bin", true,
3✔
67
      "functions", true,
3✔
68
      "internal", true,
3✔
69
      "gui", true,
3✔
70
      "system", true,
3✔
71
      "IDEasy.pdf", true,
3✔
72
      "setup", true,
3✔
73
      "setup.bat", false
1✔
74
  );
75

76
  private final UpgradeMode mode;
77

78
  /** Pattern for IDEasy SNAPSHOT versions built locally. */
79
  // ..............................................................................1..........................2........3........4
80
  private static final Pattern PATTERN_IDEASY_SNAPSHOT_VERSION = Pattern.compile("^(\\d{4}\\.\\d{2}\\.\\d{3})-(\\d{2})_(\\d{2})_(\\d{2}).*-SNAPSHOT$");
3✔
81

82
  /** Pattern for Maven/Nexus SNAPSHOT versions from downloads . */
83
  // .............................................................................1..........................2.......3.......4..........5
84
  private static final Pattern PATTERN_MAVEN_SNAPSHOT_VERSION = Pattern.compile("^(\\d{4}\\.\\d{2}\\.\\d{3})-(\\d{4})(\\d{2})(\\d{2})\\.(\\d{2})\\d{4}.*$");
4✔
85

86
  // Group numbers for PATTERN_IDEASY_SNAPSHOT_VERSION
87
  private static final int GROUP_IDEASY_BASE = 1;
88
  private static final int GROUP_IDEASY_MONTH = 2;
89
  private static final int GROUP_IDEASY_DAY = 3;
90
  private static final int GROUP_IDEASY_HOUR = 4;
91

92
  // Group numbers for PATTERN_MAVEN_SNAPSHOT_VERSION
93
  private static final int GROUP_MAVEN_BASE = 1;
94
  private static final int GROUP_MAVEN_YEAR = 2;
95
  private static final int GROUP_MAVEN_MONTH = 3;
96
  private static final int GROUP_MAVEN_DAY = 4;
97
  private static final int GROUP_MAVEN_HOUR = 5;
98

99
  /**
100
   * The constructor.
101
   *
102
   * @param context the {@link IdeContext}.
103
   */
104
  public IdeasyCommandlet(IdeContext context) {
105
    this(context, UpgradeMode.STABLE);
4✔
106
  }
1✔
107

108
  /**
109
   * The constructor.
110
   *
111
   * @param context the {@link IdeContext}.
112
   * @param mode the {@link UpgradeMode}.
113
   */
114
  public IdeasyCommandlet(IdeContext context, UpgradeMode mode) {
115

116
    super(context, TOOL_NAME, ARTIFACT, Set.of(Tag.PRODUCTIVITY, Tag.IDE));
8✔
117
    this.mode = mode;
3✔
118
  }
1✔
119

120
  @Override
121
  public VersionIdentifier getInstalledVersion() {
122

123
    return IdeVersion.getVersionIdentifier();
2✔
124
  }
125

126
  @Override
127
  public String getInstalledEdition() {
128

129
    return this.tool;
3✔
130
  }
131

132
  @Override
133
  public String getConfiguredEdition() {
134

135
    return this.tool;
3✔
136
  }
137

138
  @Override
139
  public VersionIdentifier getConfiguredVersion() {
140

141
    UpgradeMode upgradeMode = this.mode;
3✔
142
    if (upgradeMode == null) {
2!
143
      if (IdeVersion.isSnapshot()) {
2✔
144
        upgradeMode = UpgradeMode.SNAPSHOT;
3✔
145
      } else {
146
        if (IdeVersion.getVersionIdentifier().getDevelopmentPhase().isStable()) {
4!
147
          upgradeMode = UpgradeMode.STABLE;
3✔
148
        } else {
149
          upgradeMode = UpgradeMode.UNSTABLE;
×
150
        }
151
      }
152
    }
153
    return upgradeMode.getVersion();
3✔
154
  }
155

156
  @Override
157
  public Path getToolPath() {
158

159
    return this.context.getIdeInstallationPath();
4✔
160
  }
161

162
  @Override
163
  protected ToolInstallation doInstall(ToolInstallRequest request) {
164

165
    this.context.requireOnline("upgrade of IDEasy", true);
5✔
166

167
    if (IdeVersion.isUndefined() && !this.context.isForceMode()) {
6!
168
      VersionIdentifier version = IdeVersion.getVersionIdentifier();
2✔
169
      LOG.warn("You are using IDEasy version {} which indicates local development - skipping upgrade.", version);
4✔
170
      return toolAlreadyInstalled(request);
4✔
171
    }
172
    return super.doInstall(request);
×
173
  }
174

175
  /**
176
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
177
   */
178
  public VersionIdentifier getLatestVersion() {
179

180
    if (!this.context.isForceMode()) {
4!
181
      VersionIdentifier currentVersion = IdeVersion.getVersionIdentifier();
2✔
182
      if (IdeVersion.isUndefined()) {
2✔
183
        return currentVersion;
2✔
184
      }
185
    }
186
    VersionIdentifier configuredVersion = getConfiguredVersion();
3✔
187
    return getToolRepository().resolveVersion(this.tool, getConfiguredEdition(), configuredVersion, this);
10✔
188
  }
189

190
  /**
191
   * Checks if an update is available and logs according information.
192
   *
193
   * @return {@code true} if an update is available, {@code false} otherwise.
194
   */
195
  public boolean checkIfUpdateIsAvailable() {
196
    VersionIdentifier installedVersion = getInstalledVersion();
3✔
197
    IdeLogLevel.SUCCESS.log(LOG, "Your version of IDEasy is {}.", installedVersion);
10✔
198
    if (IdeVersion.isSnapshot()) {
2✔
199
      LOG.warn("You are using a SNAPSHOT version of IDEasy. For stability consider switching to a stable release via 'ide upgrade --mode=stable'");
3✔
200
    }
201
    if (this.context.isOffline()) {
4✔
202
      LOG.warn("Skipping check for newer version of IDEasy because you are offline.");
3✔
203
      return false;
2✔
204
    }
205
    VersionIdentifier latestVersion = getLatestVersion();
3✔
206
    if (IdeVersion.isSnapshot()) {
2✔
207
      if (isSameSnapshotVersion(installedVersion.toString(), latestVersion.toString())) {
7✔
208
        IdeLogLevel.SUCCESS.log(LOG, "Your are using the latest snapshot version of IDEasy and no update is available.");
4✔
209
        return false;
2✔
210
      }
211
    } else if (installedVersion.equals(latestVersion)) {
4!
212
      IdeLogLevel.SUCCESS.log(LOG, "Your are using the latest stable version of IDEasy and no update is available.");
×
213
      return false;
×
214
    }
215
    IdeLogLevel.INTERACTION.log(LOG,
14✔
216
        "Your version of IDEasy is {} but version {} is available. Please run the following command to upgrade to the latest version:\n"
217
            + "ide upgrade", installedVersion, latestVersion);
218
    return true;
2✔
219
  }
220

221
  /**
222
   * Checks if two snapshot versions represent the version
223
   *
224
   * @param installed the installed version string
225
   * @param latest the latest available version string
226
   * @return {@code true} if both versions represent the same version, {@code false} otherwise.
227
   */
228
  private boolean isSameSnapshotVersion(String installed, String latest) {
229
    if (installed == null || latest == null) {
4!
230
      return false;
×
231
    }
232

233
    Matcher installedMatcher = PATTERN_IDEASY_SNAPSHOT_VERSION.matcher(installed);
4✔
234
    Matcher latestMatcher = PATTERN_MAVEN_SNAPSHOT_VERSION.matcher(latest);
4✔
235

236
    if (!installedMatcher.matches() || !latestMatcher.matches()) {
6!
237
      return false;
2✔
238
    }
239

240
    // Compare base versions
241
    String baseInstalled = installedMatcher.group(GROUP_IDEASY_BASE);
4✔
242
    String baseLatest = latestMatcher.group(GROUP_MAVEN_BASE);
4✔
243
    if (!baseInstalled.equals(baseLatest)) {
4✔
244
      return false;
2✔
245
    }
246

247
    // Compare year
248
    String yearLatest = latestMatcher.group(GROUP_MAVEN_YEAR);
4✔
249
    String baseYear = baseInstalled.split("\\.")[0];
6✔
250
    if (!baseYear.equals(yearLatest)) {
4!
251
      return false;
×
252
    }
253

254
    // Compare MMDD.HH for both versions
255
    String keyInstalled =
2✔
256
        installedMatcher.group(GROUP_IDEASY_MONTH) + installedMatcher.group(GROUP_IDEASY_DAY) + "." + installedMatcher.group(GROUP_IDEASY_HOUR);
9✔
257
    String keyLatest = latestMatcher.group(GROUP_MAVEN_MONTH) + latestMatcher.group(GROUP_MAVEN_DAY) + "." + latestMatcher.group(GROUP_MAVEN_HOUR);
11✔
258

259
    return keyInstalled.equals(keyLatest);
4✔
260
  }
261

262
  /**
263
   * Initial installation of IDEasy.
264
   *
265
   * @param cwd the {@link Path} to the current working directory.
266
   * @see com.devonfw.tools.ide.commandlet.InstallCommandlet
267
   */
268
  public void installIdeasy(Path cwd) {
269
    Path ideRoot = determineIdeRoot(cwd);
4✔
270
    // During a fresh (MSI) installation the IDE_ROOT environment variable is not yet available, so context.getIdeRoot() would return null for the whole run.
271
    // The installation target is already known here, so we make the context consistent for any downstream code reading context.getIdeRoot() (see #1517).
272
    this.context.setIdeRoot(ideRoot);
4✔
273
    Path idePath = ideRoot.resolve(IdeContext.FOLDER_UNDERSCORE_IDE);
4✔
274
    Path installationPath = idePath.resolve(IdeContext.FOLDER_INSTALLATION);
4✔
275
    Path ideasySoftwarePath = idePath.resolve(IdeContext.FOLDER_SOFTWARE).resolve(MvnRepository.ID).resolve(IdeasyCommandlet.TOOL_NAME)
8✔
276
        .resolve(IdeasyCommandlet.TOOL_NAME);
2✔
277
    Path ideasyVersionPath = ideasySoftwarePath.resolve(IdeVersion.getVersionString());
4✔
278
    FileAccess fileAccess = this.context.getFileAccess();
4✔
279
    if (Files.isDirectory(ideasyVersionPath)) {
5✔
280
      LOG.error("IDEasy is already installed at {} - if your installation is broken, delete it manually and rerun setup!", ideasyVersionPath);
5✔
281
    } else {
282
      List<Path> installationArtifacts = new ArrayList<>();
4✔
283
      for (Map.Entry<String, Boolean> artifactEntry : REQUIRED_INSTALLATION_ARTIFACTS.entrySet()) {
11✔
284
        String artifactName = artifactEntry.getKey();
4✔
285
        boolean required = artifactEntry.getValue();
5✔
286
        boolean success = addInstallationArtifact(cwd, artifactName, required, installationArtifacts);
7✔
287
        if (!success) {
2!
288
          throw new CliException("IDEasy release is inconsistent at %s [artifact=%s]".formatted(cwd, artifactName));
×
289
        }
290
      }
1✔
291

292
      fileAccess.mkdirs(ideasyVersionPath);
3✔
293
      for (Path installationArtifact : installationArtifacts) {
10✔
294
        fileAccess.copy(installationArtifact, ideasyVersionPath);
4✔
295
      }
1✔
296
      this.context.writeVersionFile(IdeVersion.getVersionIdentifier(), ideasyVersionPath);
5✔
297
    }
298
    fileAccess.symlink(ideasyVersionPath, installationPath);
4✔
299
    addToShellRc(BASHRC, ideRoot, null);
5✔
300
    addToShellRc(ZSHRC, ideRoot, "autoload -U +X bashcompinit && bashcompinit");
5✔
301
    installIdeasyWindowsEnv(ideRoot, installationPath);
4✔
302
    installDesktopShortcut(installationPath);
3✔
303
    IdeLogLevel.SUCCESS.log(LOG, "IDEasy has been installed successfully on your system.");
4✔
304
    LOG.warn("IDEasy has been setup for new shells but it cannot work in your current shell(s).\n"
3✔
305
        + "To use it here, run 'source ~/.bashrc' (or your shell config). Otherwise, open a new terminal or reboot.");
306
  }
1✔
307

308
  private void installIdeasyWindowsEnv(Path ideRoot, Path installationPath) {
309
    if (!this.context.getSystemInfo().isWindows()) {
5✔
310
      return;
1✔
311
    }
312
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
313
    helper.setUserEnvironmentValue(IdeVariables.IDE_ROOT.getName(), ideRoot.toString());
6✔
314
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
315
    if (userPath == null) {
2!
316
      LOG.error("Could not read user PATH from registry!");
×
317
    } else {
318
      LOG.info("Found user PATH={}", userPath);
4✔
319
      Path ideasyBinPath = installationPath.resolve("bin");
4✔
320
      SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
321
      if (path.getEntries().isEmpty()) {
4!
322
        LOG.warn("ATTENTION:\n"
×
323
            + "Your user specific PATH variable seems to be empty.\n"
324
            + "You can double check this by pressing [Windows][r] and launch the program SystemPropertiesAdvanced.\n"
325
            + "Then click on 'Environment variables' and check if 'PATH' is set in the 'user variables' from the upper list.\n"
326
            + "In case 'PATH' is defined there non-empty and you get this message, please abort and give us feedback:\n"
327
            + "https://github.com/devonfw/IDEasy/issues\n"
328
            + "Otherwise all is correct and you can continue.");
329
        this.context.askToContinue("Are you sure you want to override your PATH?");
×
330
      } else {
331
        path.removeEntries(s -> s.endsWith(IDE_INSTALLATION_BIN));
7✔
332
      }
333
      path.getEntries().add(ideasyBinPath.toString());
6✔
334
      helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), path.toString());
6✔
335
      setGitLongpaths();
2✔
336
    }
337
  }
1✔
338
  private void installDesktopShortcut(Path installationPath) {
339

340
    try {
341
      if (this.context.getSystemInfo().isLinux()) {
5✔
342
        installLinuxDesktopShortcut(installationPath);
4✔
343
      } else if (this.context.getSystemInfo().isMac()) {
5✔
344
        installMacDesktopShortcut(installationPath);
4✔
345
      } else if (this.context.getSystemInfo().isWindows()) {
5!
346
        installWindowsDesktopShortcut(installationPath);
3✔
347
      }
348
    } catch (Exception e) {
×
349
      LOG.warn("Failed to create desktop shortcut.", e);
×
350
    }
1✔
351
  }
1✔
352

353
  private void installLinuxDesktopShortcut(Path installationPath) throws IOException {
354

355
    Path templateFile = installationPath.resolve("gui/linux/ideasy-gui.desktop");
4✔
356
    if (!Files.exists(templateFile)) {
5!
357
      LOG.warn("Desktop file template not found at {}. Skipping desktop shortcut creation.", templateFile);
×
358
      return;
×
359
    }
360
    Path ideasyBin = installationPath.resolve("bin/ideasy");
4✔
361
    Path logoPath = installationPath.resolve("gui/logo.png");
4✔
362
    String content = Files.readString(templateFile)
4✔
363
        .replace("@IDEASY_BIN@", ideasyBin.toString())
4✔
364
        .replace("@IDEASY_ICON@", logoPath.toString());
3✔
365

366
    Path applicationsDir = this.context.getUserHome().resolve(".local/share/applications");
6✔
367
    this.context.getFileAccess().mkdirs(applicationsDir);
5✔
368
    Path desktopFile = applicationsDir.resolve("ideasy-gui.desktop");
4✔
369
    this.context.getFileAccess().writeFileContent(content, desktopFile);
6✔
370
    this.context.getFileAccess().makeExecutable(desktopFile);
5✔
371

372
    // without this, the new entry is only visible in application menus after the next login
373
    try {
374
      this.context.newProcess().executable("update-desktop-database").addArg(applicationsDir.toString())
9✔
375
          .run(ProcessMode.DEFAULT_CAPTURE);
×
376
    } catch (Exception e) {
1✔
377
      LOG.debug("update-desktop-database failed (optional): {}", e.getMessage());
5✔
378
    }
×
379

380
    // Mark as trusted for GNOME 3.28+ so the shortcut is executable without prompting
381
    try {
382
      this.context.newProcess().executable("gio")
14✔
383
          .addArgs("set", desktopFile.toString(), "metadata::trusted", "true")
12✔
384
          .run(ProcessMode.DEFAULT_CAPTURE);
×
385
    } catch (Exception e) {
1✔
386
      LOG.debug("gio set trusted failed (optional): {}", e.getMessage());
5✔
387
    }
×
388

389
    IdeLogLevel.SUCCESS.log(LOG, "Created desktop shortcut at {}", desktopFile);
10✔
390
  }
1✔
391

392
  private void installMacDesktopShortcut(Path installationPath) {
393

394
    Path ideasyBin = installationPath.resolve("bin/ideasy");
4✔
395
    String escapedBin = ideasyBin.toString().replace("\"", "\\\"");
6✔
396
    String content = "#!/bin/bash\n\"" + escapedBin + "\" gui\n";
3✔
397
    Path applicationsDir = this.context.getUserHome().resolve("Applications");
6✔
398
    this.context.getFileAccess().mkdirs(applicationsDir);
5✔
399
    Path commandFile = applicationsDir.resolve("IDEasy.command");
4✔
400
    this.context.getFileAccess().writeFileContent(content, commandFile);
6✔
401
    this.context.getFileAccess().makeExecutable(commandFile);
5✔
402
    IdeLogLevel.SUCCESS.log(LOG, "Created macOS launcher at {}", commandFile);
10✔
403
  }
1✔
404

405
  private void installWindowsDesktopShortcut(Path installationPath) {
406

407
    Path ideasyExe = installationPath.resolve("bin\\ideasy.exe");
4✔
408
    Path icoPath = installationPath.resolve("gui\\logo.ico");
4✔
409
    // Shell Folders contains the already-expanded Desktop path, including OneDrive-redirected locations
410
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
411
    String desktopStr = helper.getRegistryValue(
5✔
412
        "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", "Desktop");
413
    Path desktopPath = (desktopStr != null && !desktopStr.isBlank()) ? Path.of(desktopStr) : this.context.getUserHome().resolve("Desktop");
8!
414
    this.context.getFileAccess().mkdirs(desktopPath);
5✔
415
    createWindowsShortcut(desktopPath.resolve("IDEasy.lnk"), ideasyExe, icoPath);
7✔
416
    String startMenuStr = helper.getRegistryValue(
5✔
417
        "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", "Programs");
418
    Path startMenu = (startMenuStr != null && !startMenuStr.isBlank()) ? Path.of(startMenuStr)
2!
419
        : this.context.getUserHome().resolve("AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs");
6✔
420
    if (Files.isDirectory(startMenu)) {
5!
421
      createWindowsShortcut(startMenu.resolve("IDEasy.lnk"), ideasyExe, icoPath);
×
422
    }
423
  }
1✔
424

425
  private static String psEscapePath(Path path) {
426
    return path.toString().replace("'", "''");
6✔
427
  }
428

429
  private void createWindowsShortcut(Path lnkPath, Path targetExe, Path icoPath) {
430

431
    String ps = "$ws = New-Object -ComObject WScript.Shell; "
1✔
432
        + "$s = $ws.CreateShortcut('" + psEscapePath(lnkPath) + "'); "
2✔
433
        + "$s.TargetPath = '" + psEscapePath(targetExe) + "'; "
2✔
434
        + "$s.Arguments = 'gui'; "
435
        + "$s.IconLocation = '" + psEscapePath(icoPath) + ",0'; "
3✔
436
        + "$s.Save()";
437
    try {
438
      this.context.newProcess().executable("powershell").addArgs("-Command", ps)
17✔
439
          .run(ProcessMode.DEFAULT_CAPTURE);
×
440
      IdeLogLevel.SUCCESS.log(LOG, "Created shortcut at {}", lnkPath);
×
441
    } catch (Exception e) {
1✔
442
      LOG.warn("Failed to create shortcut at {}.", lnkPath, e);
5✔
443
    }
×
444
  }
1✔
445

446
  private void setGitLongpaths() {
447
    this.context.getGitContext().findGitRequired();
5✔
448
    Path configPath = this.context.getUserHome().resolve(".gitconfig");
6✔
449
    FileAccess fileAccess = this.context.getFileAccess();
4✔
450
    IniFile iniFile = fileAccess.readIniFile(configPath);
4✔
451
    IniSection coreSection = iniFile.getOrCreateSection("core");
4✔
452
    coreSection.setProperty("longpaths", "true");
4✔
453
    fileAccess.writeIniFile(iniFile, configPath);
4✔
454
  }
1✔
455

456
  /**
457
   * Sets up Windows Terminal with Git Bash integration.
458
   */
459
  public void setupWindowsTerminal() {
460
    if (!this.context.getSystemInfo().isWindows()) {
×
461
      return;
×
462
    }
463
    if (!isWindowsTerminalInstalled()) {
×
464
      try {
465
        installWindowsTerminal();
×
466
      } catch (Exception e) {
×
467
        LOG.error("Failed to install Windows Terminal!", e);
×
468
      }
×
469
    }
470
    configureWindowsTerminalGitBash();
×
471
  }
×
472

473
  /**
474
   * Checks if Windows Terminal is installed.
475
   *
476
   * @return {@code true} if Windows Terminal is installed, {@code false} otherwise.
477
   */
478
  private boolean isWindowsTerminalInstalled() {
479
    try {
480
      ProcessResult result = this.context.newProcess()
×
481
          .executable("powershell")
×
482
          .addArgs("-Command", "Get-AppxPackage -Name Microsoft.WindowsTerminal")
×
483
          .run(ProcessMode.DEFAULT_CAPTURE);
×
484
      return result.isSuccessful() && !result.getOut().isEmpty();
×
485
    } catch (Exception e) {
×
486
      LOG.debug("Failed to check Windows Terminal installation.", e);
×
487
      return false;
×
488
    }
489
  }
490

491
  /**
492
   * Installs Windows Terminal using winget.
493
   */
494
  private void installWindowsTerminal() {
495
    try {
496
      LOG.info("Installing Windows Terminal...");
×
497
      ProcessResult result = this.context.newProcess()
×
498
          .executable("winget")
×
499
          .addArgs("install", "Microsoft.WindowsTerminal")
×
500
          .run(ProcessMode.DEFAULT);
×
501
      if (result.isSuccessful()) {
×
502
        IdeLogLevel.SUCCESS.log(LOG, "Windows Terminal has been installed successfully.");
×
503
      } else {
504
        LOG.warn("Failed to install Windows Terminal. Please install it manually from Microsoft Store.");
×
505
      }
506
    } catch (Exception e) {
×
507
      LOG.warn("Failed to install Windows Terminal: {}. Please install it manually from Microsoft Store.", e.toString());
×
508
    }
×
509
  }
×
510

511
  /**
512
   * Configures Git Bash integration in Windows Terminal.
513
   */
514
  protected void configureWindowsTerminalGitBash() {
515
    Path settingsPath = getWindowsTerminalSettingsPath();
3✔
516
    if (settingsPath == null || !Files.exists(settingsPath)) {
7!
517
      LOG.warn("Windows Terminal settings file not found. Cannot configure Git Bash integration.");
×
518
      return;
×
519
    }
520

521
    try {
522
      Path bashPath = this.context.findBash();
4✔
523
      if (bashPath == null) {
2!
524
        LOG.warn("Git Bash not found. Cannot configure Windows Terminal integration.");
×
525
        return;
×
526
      }
527

528
      configureGitBashProfile(settingsPath, bashPath.toString());
5✔
529
      IdeLogLevel.SUCCESS.log(LOG, "Git Bash has been configured in Windows Terminal.");
4✔
530
    } catch (Exception e) {
×
531
      LOG.warn("Failed to configure Git Bash in Windows Terminal: {}", e.getMessage());
×
532
    }
1✔
533
  }
1✔
534

535
  /**
536
   * Gets the Windows Terminal settings file path.
537
   *
538
   * @return the {@link Path} to the Windows Terminal settings file, or {@code null} if not found.
539
   */
540
  protected Path getWindowsTerminalSettingsPath() {
541
    Path localAppData = this.context.getUserHome().resolve("AppData").resolve("Local");
8✔
542
    Path packagesPath = localAppData.resolve("Packages");
4✔
543

544
    // Try the new Windows Terminal package first
545
    Path newTerminalPath = packagesPath.resolve("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
4✔
546
        .resolve("LocalState").resolve("settings.json");
4✔
547
    if (Files.exists(newTerminalPath)) {
5!
548
      return newTerminalPath;
2✔
549
    }
550

551
    // Try the old Windows Terminal Preview package
552
    Path previewPath = packagesPath.resolve("Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe")
×
553
        .resolve("LocalState").resolve("settings.json");
×
554
    if (Files.exists(previewPath)) {
×
555
      return previewPath;
×
556
    }
557

558
    return null;
×
559
  }
560

561
  /**
562
   * Configures Git Bash profile in Windows Terminal settings.
563
   *
564
   * @param settingsPath the {@link Path} to the Windows Terminal settings file.
565
   * @param bashPath the path to the Git Bash executable.
566
   */
567
  private void configureGitBashProfile(Path settingsPath, String bashPath) throws Exception {
568

569
    ObjectMapper mapper = new ObjectMapper();
4✔
570
    ObjectNode root;
571
    try (Reader reader = Files.newBufferedReader(settingsPath)) {
3✔
572
      JsonNode rootNode = mapper.readTree(reader);
4✔
573
      root = (ObjectNode) rootNode;
3✔
574
    }
575

576
    // Get or create profiles object
577
    ObjectNode profiles = (ObjectNode) root.get("profiles");
5✔
578
    if (profiles == null) {
2!
579
      profiles = mapper.createObjectNode();
×
580
      root.set("profiles", profiles);
×
581
    }
582

583
    // Get or create list array of profiles object
584
    JsonNode profilesList = profiles.get("list");
4✔
585
    if (profilesList == null || !profilesList.isArray()) {
5!
586
      profilesList = mapper.createArrayNode();
×
587
      profiles.set("list", profilesList);
×
588
    }
589

590
    // Check if Git Bash profile already exists
591
    boolean gitBashProfileExists = false;
2✔
592
    for (JsonNode profile : profilesList) {
10✔
593
      if (profile.has("name") && profile.get("name").asText().equals("Git Bash")) {
11!
594
        gitBashProfileExists = true;
×
595
        break;
×
596
      }
597
    }
1✔
598

599
    // Add Git Bash profile if it doesn't exist
600
    if (gitBashProfileExists) {
2!
601
      LOG.info("Git Bash profile already exists in {}.", settingsPath);
×
602
    } else {
603
      ObjectNode gitBashProfile = mapper.createObjectNode();
3✔
604
      String newGuid = "{2ece5bfe-50ed-5f3a-ab87-5cd4baafed2b}";
2✔
605
      String iconPath = getGitBashIconPath(bashPath);
4✔
606
      Path startingDirectory = Objects.requireNonNullElse(this.context.getIdeRoot(), this.context.getUserHome());
9✔
607

608
      gitBashProfile.put("guid", newGuid);
5✔
609
      gitBashProfile.put("name", "Git Bash");
5✔
610
      gitBashProfile.put("commandline", bashPath);
5✔
611
      gitBashProfile.put("icon", iconPath);
5✔
612
      gitBashProfile.put("startingDirectory", startingDirectory.toString());
6✔
613

614
      ((ArrayNode) profilesList).add(gitBashProfile);
5✔
615

616
      // Set Git Bash as default profile
617
      root.put("defaultProfile", newGuid);
5✔
618

619
      // Write back to file
620
      try (Writer writer = Files.newBufferedWriter(settingsPath)) {
5✔
621
        mapper.writerWithDefaultPrettyPrinter().writeValue(writer, root);
5✔
622
      }
623
    }
624
  }
1✔
625

626
  private String getGitBashIconPath(String bashPathString) {
627
    Path bashPath = Path.of(bashPathString);
5✔
628
    // "C:\\Program Files\\Git\\bin\\bash.exe"
629
    // "C:\\Program Files\\Git\\mingw64\\share\\git\\git-for-windows.ico"
630
    Path parent = bashPath.getParent();
3✔
631
    if (parent != null) {
2!
632
      Path iconPath = parent.resolve("mingw64/share/git/git-for-windows.ico");
4✔
633
      if (Files.exists(iconPath)) {
5!
634
        LOG.debug("Found git-bash icon at {}", iconPath);
×
635
        return iconPath.toString();
×
636
      }
637
      LOG.debug("Git Bash icon not found at {}. Using default icon.", iconPath);
4✔
638
    }
639
    return "ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png";
2✔
640
  }
641

642
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
643
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
644
  }
645

646
  static String removeEntryFromWindowsPath(String userPath, String suffix) {
647
    int len = userPath.length();
3✔
648
    int start = 0;
2✔
649
    while ((start >= 0) && (start < len)) {
5!
650
      int end = userPath.indexOf(';', start);
5✔
651
      if (end < 0) {
2✔
652
        end = len;
2✔
653
      }
654
      String entry = userPath.substring(start, end);
5✔
655
      if (entry.endsWith(suffix)) {
4✔
656
        String prefix = "";
2✔
657
        int offset = 1;
2✔
658
        if (start > 0) {
2✔
659
          prefix = userPath.substring(0, start - 1);
7✔
660
          offset = 0;
2✔
661
        }
662
        if (end == len) {
3✔
663
          return prefix;
2✔
664
        } else {
665
          return removeEntryFromWindowsPath(prefix + userPath.substring(end + offset), suffix);
10✔
666
        }
667
      }
668
      start = end + 1;
4✔
669
    }
1✔
670
    return userPath;
2✔
671
  }
672

673
  /**
674
   * Adds ourselves to the shell RC (run-commands) configuration file.
675
   *
676
   * @param filename the name of the RC file.
677
   * @param ideRoot the IDE_ROOT {@link Path}.
678
   */
679
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
680

681
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
682
  }
1✔
683

684
  private void removeFromShellRc(String filename, Path ideRoot) {
685

686
    modifyShellRc(filename, ideRoot, false, null);
6✔
687
  }
1✔
688

689
  /**
690
   * Adds ourselves to the shell RC (run-commands) configuration file.
691
   *
692
   * @param filename the name of the RC file.
693
   * @param ideRoot the IDE_ROOT {@link Path}.
694
   */
695
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
696

697
    if (add) {
2✔
698
      LOG.info("Configuring IDEasy in {}", filename);
5✔
699
    } else {
700
      LOG.info("Removing IDEasy from {}", filename);
4✔
701
    }
702
    Path rcFile = this.context.getUserHome().resolve(filename);
6✔
703
    FileAccess fileAccess = this.context.getFileAccess();
4✔
704
    List<String> lines = fileAccess.readFileLines(rcFile);
4✔
705
    if (lines == null) {
2✔
706
      if (!add) {
2!
707
        return;
×
708
      }
709
      lines = new ArrayList<>();
5✔
710
    } else {
711
      // since it is unspecified if the returned List may be immutable we want to get sure
712
      lines = new ArrayList<>(lines);
5✔
713
    }
714
    Iterator<String> iterator = lines.iterator();
3✔
715
    int removeCount = 0;
2✔
716
    while (iterator.hasNext()) {
3✔
717
      String line = iterator.next();
4✔
718
      line = line.trim();
3✔
719
      if (isObsoleteRcLine(line)) {
3✔
720
        LOG.info("Removing obsolete line from {}: {}", filename, line);
5✔
721
        iterator.remove();
2✔
722
        removeCount++;
2✔
723
      } else if (line.equals(extraLine)) {
4✔
724
        extraLine = null;
2✔
725
      }
726
    }
1✔
727
    if (add) {
2✔
728
      if (extraLine != null) {
2!
729
        lines.add(extraLine);
×
730
      }
731
      if (!this.context.getSystemInfo().isWindows()) {
5✔
732
        lines.add("export IDE_ROOT=\"" + WindowsPathSyntax.MSYS.format(ideRoot) + "\"");
7✔
733
      }
734
      lines.add(BASH_CODE_SOURCE_FUNCTIONS);
4✔
735
    }
736
    fileAccess.writeFileLines(lines, rcFile);
4✔
737
    LOG.debug("Successfully updated {}", filename);
4✔
738
  }
1✔
739

740
  private static boolean isObsoleteRcLine(String line) {
741
    if (line.startsWith("alias ide=")) {
4!
742
      return true;
×
743
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
744
      return true;
×
745
    } else if (line.equals("ide")) {
4✔
746
      return true;
2✔
747
    } else if (line.equals("ide init")) {
4✔
748
      return true;
2✔
749
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
750
      return true;
2✔
751
    }
752
    return false;
2✔
753
  }
754

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

757
    Path artifactPath = cwd.resolve(artifactName);
4✔
758
    if (Files.exists(artifactPath)) {
5!
759
      installationArtifacts.add(artifactPath);
5✔
760
    } else if (required) {
×
761
      LOG.error("Missing required file {}", artifactName);
×
762
      return false;
×
763
    }
764
    return true;
2✔
765
  }
766

767
  private Path determineIdeRoot(Path cwd) {
768
    Path ideRoot = this.context.getIdeRoot();
4✔
769
    if (ideRoot == null) {
2✔
770
      Path home = this.context.getUserHome();
4✔
771
      Path installRoot = home;
2✔
772
      if (this.context.getSystemInfo().isWindows()) {
5✔
773
        if (!cwd.startsWith(home)) {
4!
774
          installRoot = cwd.getRoot();
×
775
        }
776
      }
777
      ideRoot = installRoot.resolve(IdeContext.FOLDER_PROJECTS);
4✔
778
    } else {
1✔
779
      assert (Files.isDirectory(ideRoot)) : "IDE_ROOT directory does not exist!";
6!
780
    }
781
    return ideRoot;
2✔
782
  }
783

784
  /**
785
   * Uninstalls IDEasy entirely from the system.
786
   */
787
  public void uninstallIdeasy() {
788

789
    Path ideRoot = this.context.getIdeRoot();
4✔
790
    removeFromShellRc(BASHRC, ideRoot);
4✔
791
    removeFromShellRc(ZSHRC, ideRoot);
4✔
792
    Path idePath = this.context.getIdePath();
4✔
793
    uninstallIdeasyWindowsEnv(ideRoot);
3✔
794
    uninstallIdeasyIdePath(idePath);
3✔
795
    deleteDownloadCache();
2✔
796
    IdeLogLevel.SUCCESS.log(LOG, "IDEasy has been uninstalled from your system.");
4✔
797
    IdeLogLevel.INTERACTION.log(LOG, "ATTENTION:\n"
10✔
798
        + "In order to prevent data-loss, we do not delete your projects and git repositories!\n"
799
        + "To entirely get rid of IDEasy, also check your IDE_ROOT folder at:\n"
800
        + "{}", ideRoot);
801
  }
1✔
802

803
  private void deleteDownloadCache() {
804
    Path downloadPath = this.context.getDownloadPath();
4✔
805
    LOG.info("Deleting download cache from {}", downloadPath);
4✔
806
    tryDeleteDownloadCache(downloadPath);
3✔
807
    // older versions kept the cache under ~/Downloads/ide - clean that up too if it's still around
808
    Path legacy = this.context.getUserHome().resolve("Downloads/ide");
6✔
809
    if (!legacy.equals(downloadPath) && Files.exists(legacy)) {
4!
810
      LOG.info("Deleting legacy download cache from {}", legacy);
×
811
      tryDeleteDownloadCache(legacy);
×
812
    }
813
  }
1✔
814

815
  private void tryDeleteDownloadCache(Path path) {
816
    try {
817
      this.context.getFileAccess().delete(path);
5✔
818
    } catch (IllegalStateException e) {
×
819
      // best effort - on macOS ~/Downloads can deny access (EPERM), don't fail the whole uninstall over it
820
      String cause = (e.getCause() != null) ? e.getCause().getMessage() : e.getMessage();
×
821
      LOG.warn("Could not delete download cache at {} ({}). The folder will be left in place; you can remove it manually via Finder.", path, cause);
×
822
    }
1✔
823
  }
1✔
824

825
  private void uninstallIdeasyIdePath(Path idePath) {
826
    if (this.context.getSystemInfo().isWindows()) {
5✔
827
      Path bash = this.context.findBash();
4✔
828
      if (bash == null) {
2!
829
        LOG.warn("Could not find bash for asynchronous deletion of {}. Falling back to direct deletion.", idePath);
×
830
        this.context.getFileAccess().delete(idePath);
×
831
        return;
×
832
      }
833
      this.context.newProcess().executable(bash).addArgs("-c",
17✔
834
          "sleep 10 && rm -rf \"" + WindowsPathSyntax.MSYS.format(idePath) + "\"").run(ProcessMode.BACKGROUND);
5✔
835
      IdeLogLevel.INTERACTION.log(LOG,
10✔
836
          "To prevent windows file locking errors, we perform an asynchronous deletion of {} in background now.\n"
837
              + "Please close all terminals and wait a minute for the deletion to complete before running other commands.", idePath);
838
    } else {
1✔
839
      LOG.info("Finally deleting {}", idePath);
4✔
840
      this.context.getFileAccess().delete(idePath);
5✔
841
    }
842
  }
1✔
843

844
  private void uninstallIdeasyWindowsEnv(Path ideRoot) {
845
    if (!this.context.getSystemInfo().isWindows()) {
5✔
846
      return;
1✔
847
    }
848
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
849
    helper.removeUserEnvironmentValue(IdeVariables.IDE_ROOT.getName());
4✔
850
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
851
    if (userPath == null) {
2!
852
      LOG.error("Could not read user PATH from registry!");
×
853
    } else {
854
      LOG.info("Found user PATH={}", userPath);
4✔
855
      String newUserPath = userPath;
2✔
856
      if (!userPath.isEmpty()) {
3!
857
        SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
858
        path.removeEntries(s -> s.endsWith(IDE_BIN) || s.endsWith(IDE_INSTALLATION_BIN));
15!
859
        newUserPath = path.toString();
3✔
860
      }
861
      if (newUserPath.equals(userPath)) {
4!
862
        LOG.error("Could not find IDEasy in PATH:\n{}", userPath);
×
863
      } else {
864
        helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), newUserPath);
5✔
865
      }
866
    }
867
  }
1✔
868
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc