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

devonfw / IDEasy / 30450098251

29 Jul 2026 12:04PM UTC coverage: 72.632% (+0.04%) from 72.593%
30450098251

Pull #2232

github

web-flow
Merge d7bac434a into ec264ffca
Pull Request #2232: #1135: Add PowerShell environment initialization

5016 of 7635 branches covered (65.7%)

Branch coverage included in aggregate %.

12922 of 17062 relevant lines covered (75.74%)

3.21 hits per line

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

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

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

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

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

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

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

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

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

55
  private static final String POWERSHELL_CODE_SOURCE_FUNCTIONS =
56
      ". \"$env:IDE_ROOT\\_ide\\installation\\functions.ps1\"";
57

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

65

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

79
  private final UpgradeMode mode;
80

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

85
  /** Pattern for Maven/Nexus SNAPSHOT versions from downloads . */
86
  // .............................................................................1..........................2.......3.......4..........5
87
  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✔
88

89
  // Group numbers for PATTERN_IDEASY_SNAPSHOT_VERSION
90
  private static final int GROUP_IDEASY_BASE = 1;
91
  private static final int GROUP_IDEASY_MONTH = 2;
92
  private static final int GROUP_IDEASY_DAY = 3;
93
  private static final int GROUP_IDEASY_HOUR = 4;
94

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

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

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

119
    super(context, TOOL_NAME, ARTIFACT, Set.of(Tag.PRODUCTIVITY, Tag.IDE));
8✔
120
    this.mode = mode;
3✔
121
  }
1✔
122

123
  @Override
124
  public VersionIdentifier getInstalledVersion() {
125

126
    return IdeVersion.getVersionIdentifier();
2✔
127
  }
128

129
  @Override
130
  public String getInstalledEdition() {
131

132
    return this.tool;
3✔
133
  }
134

135
  @Override
136
  public String getConfiguredEdition() {
137

138
    return this.tool;
3✔
139
  }
140

141
  @Override
142
  public VersionIdentifier getConfiguredVersion() {
143

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

159
  @Override
160
  public Path getToolPath() {
161

162
    return this.context.getIdeInstallationPath();
4✔
163
  }
164

165
  @Override
166
  protected ToolInstallation doInstall(ToolInstallRequest request) {
167

168
    this.context.requireOnline("upgrade of IDEasy", true);
5✔
169

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

178
  /**
179
   * @return the latest released {@link VersionIdentifier version} of IDEasy.
180
   */
181
  public VersionIdentifier getLatestVersion() {
182

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

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

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

236
    Matcher installedMatcher = PATTERN_IDEASY_SNAPSHOT_VERSION.matcher(installed);
4✔
237
    Matcher latestMatcher = PATTERN_MAVEN_SNAPSHOT_VERSION.matcher(latest);
4✔
238

239
    if (!installedMatcher.matches() || !latestMatcher.matches()) {
6!
240
      return false;
2✔
241
    }
242

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

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

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

262
    return keyInstalled.equals(keyLatest);
4✔
263
  }
264

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

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

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

343
  private void installPowerShellProfile() {
344

345
    modifyPowerShellProfiles(true);
3✔
346
  }
1✔
347

348
  private void uninstallPowerShellProfile() {
349

350
    modifyPowerShellProfiles(false);
3✔
351
  }
1✔
352

353
  private void modifyPowerShellProfiles(boolean add) {
354

355
    if (!this.context.getSystemInfo().isWindows()) {
5✔
356
      return;
1✔
357
    }
358

359
    // Windows PowerShell 5.x and PowerShell 7+ have different profile locations.
360
    modifyPowerShellProfile("powershell", add);
4✔
361
    modifyPowerShellProfile("pwsh", add);
4✔
362
  }
1✔
363

364
  private void modifyPowerShellProfile(String executable, boolean add) {
365

366
    Path profilePath = getPowerShellProfilePath(executable);
4✔
367
    if (profilePath == null) {
2✔
368
      return;
1✔
369
    }
370

371
    if (add) {
2✔
372
      LOG.info("Configuring IDEasy in PowerShell profile {}", profilePath);
5✔
373
    } else {
374
      LOG.info("Removing IDEasy from PowerShell profile {}", profilePath);
4✔
375
    }
376

377
    FileAccess fileAccess = this.context.getFileAccess();
4✔
378
    List<String> lines = fileAccess.readFileLines(profilePath);
4✔
379

380
    if (lines == null) {
2✔
381
      if (!add) {
2!
382
        return;
×
383
      }
384
      lines = new ArrayList<>();
5✔
385
    } else {
386
      lines = new ArrayList<>(lines);
5✔
387
    }
388

389
    Iterator<String> iterator = lines.iterator();
3✔
390
    boolean alreadyConfigured = false;
2✔
391

392
    while (iterator.hasNext()) {
3✔
393
      String line = iterator.next().trim();
5✔
394

395
      if (line.equals(POWERSHELL_CODE_SOURCE_FUNCTIONS)) {
4!
396
        if (add) {
2✔
397
          alreadyConfigured = true;
3✔
398
        } else {
399
          iterator.remove();
2✔
400
        }
401
      }
402
    }
1✔
403

404
    if (add && !alreadyConfigured) {
4✔
405
      lines.add(POWERSHELL_CODE_SOURCE_FUNCTIONS);
4✔
406
    }
407

408
    Path parent = profilePath.getParent();
3✔
409
    if (parent != null) {
2!
410
      fileAccess.mkdirs(parent);
3✔
411
    }
412

413
    fileAccess.writeFileLines(lines, profilePath);
4✔
414
    LOG.debug("Successfully updated PowerShell profile {}", profilePath);
4✔
415
  }
1✔
416

417
  private Path getPowerShellProfilePath(String executable) {
418

419
    try {
420
      ProcessResult result = this.context.newProcess()
4✔
421
          .executable(executable)
15✔
422
          .addArgs("-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts")
2✔
423
          .run(ProcessMode.DEFAULT_CAPTURE);
2✔
424

425
      if (!result.isSuccessful()) {
3!
426
        LOG.debug("{} is not available or its profile could not be determined.", executable);
×
427
        return null;
×
428
      }
429

430
      List<String> output = result.getOut();
3✔
431
      if (output == null || output.isEmpty()) {
5!
432
        LOG.debug("{} returned no PowerShell profile path.", executable);
×
433
        return null;
×
434
      }
435

436
      String profilePath = output.stream()
3✔
437
          .map(String::strip)
2✔
438
          .filter(line -> !line.isEmpty())
7!
439
          .findFirst()
2✔
440
          .orElse(null);
3✔
441

442
      if (profilePath == null) {
2!
443
        LOG.debug("{} returned no PowerShell profile path.", executable);
×
444
        return null;
×
445
      }
446

447
      return Path.of(profilePath);
5✔
448

449
    } catch (Exception e) {
1✔
450
      // pwsh is optional. Windows PowerShell normally exists on supported Windows versions.
451
      LOG.debug("Could not determine profile for {}: {}", executable, e.getMessage());
6✔
452
      return null;
2✔
453
    }
454
  }
455

456
  private void setGitLongpaths() {
457
    this.context.getGitContext().findGitRequired();
5✔
458
    Path configPath = this.context.getUserHome().resolve(".gitconfig");
6✔
459
    FileAccess fileAccess = this.context.getFileAccess();
4✔
460
    IniFile iniFile = fileAccess.readIniFile(configPath);
4✔
461
    IniSection coreSection = iniFile.getOrCreateSection("core");
4✔
462
    coreSection.setProperty("longpaths", "true");
4✔
463
    fileAccess.writeIniFile(iniFile, configPath);
4✔
464
  }
1✔
465

466
  /**
467
   * Sets up Windows Terminal with Git Bash integration.
468
   */
469
  public void setupWindowsTerminal() {
470
    if (!this.context.getSystemInfo().isWindows()) {
×
471
      return;
×
472
    }
473
    if (!isWindowsTerminalInstalled()) {
×
474
      try {
475
        installWindowsTerminal();
×
476
      } catch (Exception e) {
×
477
        LOG.error("Failed to install Windows Terminal!", e);
×
478
      }
×
479
    }
480
    configureWindowsTerminalGitBash();
×
481
  }
×
482

483
  /**
484
   * Checks if Windows Terminal is installed.
485
   *
486
   * @return {@code true} if Windows Terminal is installed, {@code false} otherwise.
487
   */
488
  private boolean isWindowsTerminalInstalled() {
489
    try {
490
      ProcessResult result = this.context.newProcess()
×
491
          .executable("powershell")
×
492
          .addArgs("-Command", "Get-AppxPackage -Name Microsoft.WindowsTerminal")
×
493
          .run(ProcessMode.DEFAULT_CAPTURE);
×
494
      return result.isSuccessful() && !result.getOut().isEmpty();
×
495
    } catch (Exception e) {
×
496
      LOG.debug("Failed to check Windows Terminal installation.", e);
×
497
      return false;
×
498
    }
499
  }
500

501
  /**
502
   * Installs Windows Terminal using winget.
503
   */
504
  private void installWindowsTerminal() {
505
    try {
506
      LOG.info("Installing Windows Terminal...");
×
507
      ProcessResult result = this.context.newProcess()
×
508
          .executable("winget")
×
509
          .addArgs("install", "Microsoft.WindowsTerminal")
×
510
          .run(ProcessMode.DEFAULT);
×
511
      if (result.isSuccessful()) {
×
512
        IdeLogLevel.SUCCESS.log(LOG, "Windows Terminal has been installed successfully.");
×
513
      } else {
514
        LOG.warn("Failed to install Windows Terminal. Please install it manually from Microsoft Store.");
×
515
      }
516
    } catch (Exception e) {
×
517
      LOG.warn("Failed to install Windows Terminal: {}. Please install it manually from Microsoft Store.", e.toString());
×
518
    }
×
519
  }
×
520

521
  /**
522
   * Configures Git Bash integration in Windows Terminal.
523
   */
524
  protected void configureWindowsTerminalGitBash() {
525
    Path settingsPath = getWindowsTerminalSettingsPath();
3✔
526
    if (settingsPath == null || !Files.exists(settingsPath)) {
7!
527
      LOG.warn("Windows Terminal settings file not found. Cannot configure Git Bash integration.");
×
528
      return;
×
529
    }
530

531
    try {
532
      Path bashPath = this.context.findBash();
4✔
533
      if (bashPath == null) {
2!
534
        LOG.warn("Git Bash not found. Cannot configure Windows Terminal integration.");
×
535
        return;
×
536
      }
537

538
      configureGitBashProfile(settingsPath, bashPath.toString());
5✔
539
      IdeLogLevel.SUCCESS.log(LOG, "Git Bash has been configured in Windows Terminal.");
4✔
540
    } catch (Exception e) {
×
541
      LOG.warn("Failed to configure Git Bash in Windows Terminal: {}", e.getMessage());
×
542
    }
1✔
543
  }
1✔
544

545
  /**
546
   * Gets the Windows Terminal settings file path.
547
   *
548
   * @return the {@link Path} to the Windows Terminal settings file, or {@code null} if not found.
549
   */
550
  protected Path getWindowsTerminalSettingsPath() {
551
    Path localAppData = this.context.getUserHome().resolve("AppData").resolve("Local");
8✔
552
    Path packagesPath = localAppData.resolve("Packages");
4✔
553

554
    // Try the new Windows Terminal package first
555
    Path newTerminalPath = packagesPath.resolve("Microsoft.WindowsTerminal_8wekyb3d8bbwe")
4✔
556
        .resolve("LocalState").resolve("settings.json");
4✔
557
    if (Files.exists(newTerminalPath)) {
5!
558
      return newTerminalPath;
2✔
559
    }
560

561
    // Try the old Windows Terminal Preview package
562
    Path previewPath = packagesPath.resolve("Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe")
×
563
        .resolve("LocalState").resolve("settings.json");
×
564
    if (Files.exists(previewPath)) {
×
565
      return previewPath;
×
566
    }
567

568
    return null;
×
569
  }
570

571
  /**
572
   * Configures Git Bash profile in Windows Terminal settings.
573
   *
574
   * @param settingsPath the {@link Path} to the Windows Terminal settings file.
575
   * @param bashPath the path to the Git Bash executable.
576
   */
577
  private void configureGitBashProfile(Path settingsPath, String bashPath) throws Exception {
578

579
    ObjectMapper mapper = new ObjectMapper();
4✔
580
    ObjectNode root;
581
    try (Reader reader = Files.newBufferedReader(settingsPath)) {
3✔
582
      JsonNode rootNode = mapper.readTree(reader);
4✔
583
      root = (ObjectNode) rootNode;
3✔
584
    }
585

586
    // Get or create profiles object
587
    ObjectNode profiles = (ObjectNode) root.get("profiles");
5✔
588
    if (profiles == null) {
2!
589
      profiles = mapper.createObjectNode();
×
590
      root.set("profiles", profiles);
×
591
    }
592

593
    // Get or create list array of profiles object
594
    JsonNode profilesList = profiles.get("list");
4✔
595
    if (profilesList == null || !profilesList.isArray()) {
5!
596
      profilesList = mapper.createArrayNode();
×
597
      profiles.set("list", profilesList);
×
598
    }
599

600
    // Check if Git Bash profile already exists
601
    boolean gitBashProfileExists = false;
2✔
602
    for (JsonNode profile : profilesList) {
10✔
603
      if (profile.has("name") && profile.get("name").asText().equals("Git Bash")) {
11!
604
        gitBashProfileExists = true;
×
605
        break;
×
606
      }
607
    }
1✔
608

609
    // Add Git Bash profile if it doesn't exist
610
    if (gitBashProfileExists) {
2!
611
      LOG.info("Git Bash profile already exists in {}.", settingsPath);
×
612
    } else {
613
      ObjectNode gitBashProfile = mapper.createObjectNode();
3✔
614
      String newGuid = "{2ece5bfe-50ed-5f3a-ab87-5cd4baafed2b}";
2✔
615
      String iconPath = getGitBashIconPath(bashPath);
4✔
616
      Path startingDirectory = Objects.requireNonNullElse(this.context.getIdeRoot(), this.context.getUserHome());
9✔
617

618
      gitBashProfile.put("guid", newGuid);
5✔
619
      gitBashProfile.put("name", "Git Bash");
5✔
620
      gitBashProfile.put("commandline", bashPath);
5✔
621
      gitBashProfile.put("icon", iconPath);
5✔
622
      gitBashProfile.put("startingDirectory", startingDirectory.toString());
6✔
623

624
      ((ArrayNode) profilesList).add(gitBashProfile);
5✔
625

626
      // Set Git Bash as default profile
627
      root.put("defaultProfile", newGuid);
5✔
628

629
      // Write back to file
630
      try (Writer writer = Files.newBufferedWriter(settingsPath)) {
5✔
631
        mapper.writerWithDefaultPrettyPrinter().writeValue(writer, root);
5✔
632
      }
633
    }
634
  }
1✔
635

636
  private String getGitBashIconPath(String bashPathString) {
637
    Path bashPath = Path.of(bashPathString);
5✔
638
    // "C:\\Program Files\\Git\\bin\\bash.exe"
639
    // "C:\\Program Files\\Git\\mingw64\\share\\git\\git-for-windows.ico"
640
    Path parent = bashPath.getParent();
3✔
641
    if (parent != null) {
2!
642
      Path iconPath = parent.resolve("mingw64/share/git/git-for-windows.ico");
4✔
643
      if (Files.exists(iconPath)) {
5!
644
        LOG.debug("Found git-bash icon at {}", iconPath);
×
645
        return iconPath.toString();
×
646
      }
647
      LOG.debug("Git Bash icon not found at {}. Using default icon.", iconPath);
4✔
648
    }
649
    return "ms-appx:///ProfileIcons/{0caa0dad-35be-5f56-a8ff-afceeeaa6101}.png";
2✔
650
  }
651

652
  static String removeObsoleteEntryFromWindowsPath(String userPath) {
653
    return removeEntryFromWindowsPath(userPath, IDE_BIN);
4✔
654
  }
655

656
  static String removeEntryFromWindowsPath(String userPath, String suffix) {
657
    int len = userPath.length();
3✔
658
    int start = 0;
2✔
659
    while ((start >= 0) && (start < len)) {
5!
660
      int end = userPath.indexOf(';', start);
5✔
661
      if (end < 0) {
2✔
662
        end = len;
2✔
663
      }
664
      String entry = userPath.substring(start, end);
5✔
665
      if (entry.endsWith(suffix)) {
4✔
666
        String prefix = "";
2✔
667
        int offset = 1;
2✔
668
        if (start > 0) {
2✔
669
          prefix = userPath.substring(0, start - 1);
7✔
670
          offset = 0;
2✔
671
        }
672
        if (end == len) {
3✔
673
          return prefix;
2✔
674
        } else {
675
          return removeEntryFromWindowsPath(prefix + userPath.substring(end + offset), suffix);
10✔
676
        }
677
      }
678
      start = end + 1;
4✔
679
    }
1✔
680
    return userPath;
2✔
681
  }
682

683
  /**
684
   * Adds ourselves to the shell RC (run-commands) configuration file.
685
   *
686
   * @param filename the name of the RC file.
687
   * @param ideRoot the IDE_ROOT {@link Path}.
688
   */
689
  private void addToShellRc(String filename, Path ideRoot, String extraLine) {
690

691
    modifyShellRc(filename, ideRoot, true, extraLine);
6✔
692
  }
1✔
693

694
  private void removeFromShellRc(String filename, Path ideRoot) {
695

696
    modifyShellRc(filename, ideRoot, false, null);
6✔
697
  }
1✔
698

699
  /**
700
   * Adds ourselves to the shell RC (run-commands) configuration file.
701
   *
702
   * @param filename the name of the RC file.
703
   * @param ideRoot the IDE_ROOT {@link Path}.
704
   */
705
  private void modifyShellRc(String filename, Path ideRoot, boolean add, String extraLine) {
706

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

750
  private static boolean isObsoleteRcLine(String line) {
751
    if (line.startsWith("alias ide=")) {
4!
752
      return true;
×
753
    } else if (line.startsWith("export IDE_ROOT=")) {
4!
754
      return true;
×
755
    } else if (line.equals("ide")) {
4✔
756
      return true;
2✔
757
    } else if (line.equals("ide init")) {
4✔
758
      return true;
2✔
759
    } else if (line.startsWith("source \"$IDE_ROOT/_ide/")) {
4✔
760
      return true;
2✔
761
    }
762
    return false;
2✔
763
  }
764

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

767
    Path artifactPath = cwd.resolve(artifactName);
4✔
768
    if (Files.exists(artifactPath)) {
5!
769
      installationArtifacts.add(artifactPath);
5✔
770
    } else if (required) {
×
771
      LOG.error("Missing required file {}", artifactName);
×
772
      return false;
×
773
    }
774
    return true;
2✔
775
  }
776

777
  private Path determineIdeRoot(Path cwd) {
778
    Path ideRoot = this.context.getIdeRoot();
4✔
779
    if (ideRoot == null) {
2✔
780
      Path home = this.context.getUserHome();
4✔
781
      Path installRoot = home;
2✔
782
      if (this.context.getSystemInfo().isWindows()) {
5✔
783
        if (!cwd.startsWith(home)) {
4!
784
          installRoot = cwd.getRoot();
×
785
        }
786
      }
787
      ideRoot = installRoot.resolve(IdeContext.FOLDER_PROJECTS);
4✔
788
    } else {
1✔
789
      assert (Files.isDirectory(ideRoot)) : "IDE_ROOT directory does not exist!";
6!
790
    }
791
    return ideRoot;
2✔
792
  }
793

794
  /**
795
   * Uninstalls IDEasy entirely from the system.
796
   */
797
  public void uninstallIdeasy() {
798

799
    Path ideRoot = this.context.getIdeRoot();
4✔
800
    removeFromShellRc(BASHRC, ideRoot);
4✔
801
    removeFromShellRc(ZSHRC, ideRoot);
4✔
802
    Path idePath = this.context.getIdePath();
4✔
803
    uninstallIdeasyWindowsEnv(ideRoot);
3✔
804
    uninstallPowerShellProfile();
2✔
805
    uninstallIdeasyIdePath(idePath);
3✔
806
    deleteDownloadCache();
2✔
807
    IdeLogLevel.SUCCESS.log(LOG, "IDEasy has been uninstalled from your system.");
4✔
808
    IdeLogLevel.INTERACTION.log(LOG, "ATTENTION:\n"
10✔
809
        + "In order to prevent data-loss, we do not delete your projects and git repositories!\n"
810
        + "To entirely get rid of IDEasy, also check your IDE_ROOT folder at:\n"
811
        + "{}", ideRoot);
812
  }
1✔
813

814
  private void deleteDownloadCache() {
815
    Path downloadPath = this.context.getDownloadPath();
4✔
816
    LOG.info("Deleting download cache from {}", downloadPath);
4✔
817
    tryDeleteDownloadCache(downloadPath);
3✔
818
    // older versions kept the cache under ~/Downloads/ide - clean that up too if it's still around
819
    Path legacy = this.context.getUserHome().resolve("Downloads/ide");
6✔
820
    if (!legacy.equals(downloadPath) && Files.exists(legacy)) {
4!
821
      LOG.info("Deleting legacy download cache from {}", legacy);
×
822
      tryDeleteDownloadCache(legacy);
×
823
    }
824
  }
1✔
825

826
  private void tryDeleteDownloadCache(Path path) {
827
    try {
828
      this.context.getFileAccess().delete(path);
5✔
829
    } catch (IllegalStateException e) {
×
830
      // best effort - on macOS ~/Downloads can deny access (EPERM), don't fail the whole uninstallation over it
831
      String cause = (e.getCause() != null) ? e.getCause().getMessage() : e.getMessage();
×
832
      LOG.warn("Could not delete download cache at {} ({}). The folder will be left in place; you can remove it manually via Finder.", path, cause);
×
833
    }
1✔
834
  }
1✔
835

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

855
  private void uninstallIdeasyWindowsEnv(Path ideRoot) {
856
    if (!this.context.getSystemInfo().isWindows()) {
5✔
857
      return;
1✔
858
    }
859
    WindowsHelper helper = WindowsHelper.get(this.context);
4✔
860
    helper.removeUserEnvironmentValue(IdeVariables.IDE_ROOT.getName());
4✔
861
    String userPath = helper.getUserEnvironmentValue(IdeVariables.PATH.getName());
5✔
862
    if (userPath == null) {
2!
863
      LOG.error("Could not read user PATH from registry!");
×
864
    } else {
865
      LOG.info("Found user PATH={}", userPath);
4✔
866
      String newUserPath = userPath;
2✔
867
      if (!userPath.isEmpty()) {
3!
868
        SimpleSystemPath path = SimpleSystemPath.of(userPath, ';');
4✔
869
        path.removeEntries(s -> s.endsWith(IDE_BIN) || s.endsWith(IDE_INSTALLATION_BIN));
15!
870
        newUserPath = path.toString();
3✔
871
      }
872
      if (newUserPath.equals(userPath)) {
4!
873
        LOG.error("Could not find IDEasy in PATH:\n{}", userPath);
×
874
      } else {
875
        helper.setUserEnvironmentValue(IdeVariables.PATH.getName(), newUserPath);
5✔
876
      }
877
    }
878
  }
1✔
879
}
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