diff --git a/platform/platform-impl/internal/src/com/intellij/internal/statistic/collectors/fus/build/BuildToolsCollector.kt b/platform/platform-impl/internal/src/com/intellij/internal/statistic/collectors/fus/build/BuildToolsCollector.kt index a1ba585d7001..8ffb8d8f7119 100644 --- a/platform/platform-impl/internal/src/com/intellij/internal/statistic/collectors/fus/build/BuildToolsCollector.kt +++ b/platform/platform-impl/internal/src/com/intellij/internal/statistic/collectors/fus/build/BuildToolsCollector.kt @@ -7,7 +7,7 @@ import com.intellij.internal.statistic.eventLog.events.EventFields import com.intellij.internal.statistic.eventLog.events.EventId1 import com.intellij.internal.statistic.service.fus.collectors.ApplicationUsagesCollector import com.intellij.openapi.progress.checkCanceled -import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.EnvironmentScanner +import com.intellij.openapi.util.EnvironmentScanner import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/platform/platform-impl/src/com/intellij/openapi/updateSettings/impl/pluginsAdvertisement/EnvironmentDependencyCollector.kt b/platform/platform-impl/src/com/intellij/openapi/updateSettings/impl/pluginsAdvertisement/EnvironmentDependencyCollector.kt index b840cb2bf7d2..b8069ec29a94 100644 --- a/platform/platform-impl/src/com/intellij/openapi/updateSettings/impl/pluginsAdvertisement/EnvironmentDependencyCollector.kt +++ b/platform/platform-impl/src/com/intellij/openapi/updateSettings/impl/pluginsAdvertisement/EnvironmentDependencyCollector.kt @@ -5,15 +5,7 @@ import com.intellij.ide.IdeBundle import com.intellij.ide.plugins.DependencyCollector import com.intellij.ide.plugins.DependencyInformation import com.intellij.openapi.project.Project -import com.intellij.util.EnvironmentUtil -import com.intellij.util.system.OS -import org.jetbrains.annotations.ApiStatus -import java.nio.file.FileSystems -import java.nio.file.Files -import java.nio.file.InvalidPathException -import java.nio.file.Path -import kotlin.io.path.isExecutable -import kotlin.io.path.isRegularFile +import com.intellij.openapi.util.EnvironmentScanner internal class EnvironmentDependencyCollector : DependencyCollector { private val ALLOWED_EXECUTABLES: List = listOf( @@ -35,43 +27,4 @@ internal class EnvironmentDependencyCollector : DependencyCollector { .filter { EnvironmentScanner.hasToolInLocalPath(pathNames, it) } .map { DependencyInformation(it, IdeBundle.message("plugins.configurable.suggested.features.executable", it)) } } -} - -@ApiStatus.Internal -object EnvironmentScanner { - fun getPathNames(): List { - val fs = FileSystems.getDefault() - - @Suppress("IO_FILE_USAGE") - val pathDelimiter = java.io.File.pathSeparatorChar - - val pathNames = EnvironmentUtil.getEnvironmentMap()["PATH"]?.split(pathDelimiter) - ?.mapNotNull { - try { - fs.getPath(it) - } - catch (_: InvalidPathException) { - null - } - } - ?.filter(Files::exists) - return pathNames ?: emptyList() - } - - fun hasToolInLocalPath(pathNames: List, executableWithoutExt: String): Boolean { - val baseNames = if (OS.CURRENT == OS.Windows) { - sequenceOf(".bat", ".cmd", ".com", ".exe") - .map { exeSuffix -> executableWithoutExt + exeSuffix } - } - else { - sequenceOf(executableWithoutExt) - } - - return pathNames.asSequence() - .flatMap { pathEntry -> - baseNames.map { basename -> pathEntry.resolve(basename) } - } - .filter(Path::isRegularFile) - .any(Path::isExecutable) - } -} +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/openapi/util/EnvironmentScanner.kt b/platform/platform-impl/src/com/intellij/openapi/util/EnvironmentScanner.kt new file mode 100644 index 000000000000..c552702d79e0 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/util/EnvironmentScanner.kt @@ -0,0 +1,53 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.openapi.util + +import com.intellij.util.EnvironmentUtil +import com.intellij.util.system.LowLevelLocalMachineAccess +import com.intellij.util.system.OS +import org.jetbrains.annotations.ApiStatus +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.InvalidPathException +import java.nio.file.Path +import kotlin.io.path.isExecutable +import kotlin.io.path.isRegularFile + +@ApiStatus.Internal +object EnvironmentScanner { + fun getPathNames(): List { + val fs = FileSystems.getDefault() + + @Suppress("IO_FILE_USAGE") + val pathDelimiter = java.io.File.pathSeparatorChar + + val pathNames = EnvironmentUtil.getEnvironmentMap()["PATH"]?.split(pathDelimiter) + ?.mapNotNull { + try { + fs.getPath(it) + } + catch (_: InvalidPathException) { + null + } + } + ?.filter(Files::exists) + return pathNames ?: emptyList() + } + + @OptIn(LowLevelLocalMachineAccess::class) + fun hasToolInLocalPath(pathNames: List, executableWithoutExt: String): Boolean { + val baseNames = if (OS.CURRENT == OS.Windows) { + sequenceOf(".bat", ".cmd", ".com", ".exe") + .map { exeSuffix -> executableWithoutExt + exeSuffix } + } + else { + sequenceOf(executableWithoutExt) + } + + return pathNames.asSequence() + .flatMap { pathEntry -> + baseNames.map { basename -> pathEntry.resolve(basename) } + } + .filter(Path::isRegularFile) + .any(Path::isExecutable) + } +} diff --git a/platform/platform-tests/testSrc/com/intellij/ide/plugins/advertiser/EnvironmentScannerTest.kt b/platform/platform-tests/testSrc/com/intellij/ide/plugins/advertiser/EnvironmentScannerTest.kt index affc83a9301f..56421641e6e2 100644 --- a/platform/platform-tests/testSrc/com/intellij/ide/plugins/advertiser/EnvironmentScannerTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/ide/plugins/advertiser/EnvironmentScannerTest.kt @@ -1,7 +1,7 @@ // Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.ide.plugins.advertiser -import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.EnvironmentScanner +import com.intellij.openapi.util.EnvironmentScanner import com.intellij.util.EnvironmentUtil import com.intellij.util.system.LowLevelLocalMachineAccess import com.intellij.util.system.OS diff --git a/plugins/terminal/backend/resources/intellij.terminal.backend.xml b/plugins/terminal/backend/resources/intellij.terminal.backend.xml index 376f0a5ec228..4dd44cc3db7b 100644 --- a/plugins/terminal/backend/resources/intellij.terminal.backend.xml +++ b/plugins/terminal/backend/resources/intellij.terminal.backend.xml @@ -10,6 +10,7 @@ + diff --git a/plugins/terminal/backend/src/com/intellij/terminal/backend/CliUtilInstallCollector.kt b/plugins/terminal/backend/src/com/intellij/terminal/backend/CliUtilInstallCollector.kt new file mode 100644 index 000000000000..0a787c39d3f9 --- /dev/null +++ b/plugins/terminal/backend/src/com/intellij/terminal/backend/CliUtilInstallCollector.kt @@ -0,0 +1,225 @@ +package com.intellij.terminal.backend + +import com.intellij.internal.statistic.beans.MetricEvent +import com.intellij.internal.statistic.eventLog.EventLogGroup +import com.intellij.internal.statistic.eventLog.events.EventFields +import com.intellij.internal.statistic.eventLog.events.EventId1 +import com.intellij.internal.statistic.service.fus.collectors.ApplicationUsagesCollector +import com.intellij.openapi.util.EnvironmentScanner +import com.intellij.util.SystemProperties +import java.nio.file.FileSystems +import java.nio.file.Files +import java.nio.file.InvalidPathException +import java.nio.file.Path + +internal class CliUtilInstallCollector : ApplicationUsagesCollector() { + private val GROUP: EventLogGroup = EventLogGroup("cli.tools", 1) + + @Suppress("SpellCheckingInspection") + private val CLI_EXECUTABLES: List = listOf( + "aliyun", + "amplify", + "ansible", + "argocd", + "aws", + "az", + "bpftrace", + "buildah", + "bun", + "cargo", + "cdk", + "cfn", + "chaos-mesh", + "chef", + "cilium", + "circleci", + "code", + "codium", + "colima", + "conftest", + "consul", + "crane", + "crictl", + "ctr", + "datadog-agent", + "devcontainer-cli", + "devspace", + "docker", + "doctl", + "doppler", + "drone", + "eb", + "eksctl", + "envoy", + "etcdctl", + "fleet", + "flux", + "fly", + "gcloud", + "gem", + "git", + "gitpod", + "glab", + "gradle", + "grafana", + "hadolint", + "harbor", + "hcloud", + "helm", + "helmfile", + "hubble", + "ibmcloud", + "istioctl", + "jenkins-cli", + "jf", + "jq", + "k3d", + "k3s", + "k9s", + "kibana", + "kind", + "kubeconform", + "kubectl", + "kubectx", + "kubefwd", + "kubens", + "kubeseal", + "kubetail", + "kustomize", + "lima", + "localstack", + "lxc", + "lxd", + "maven", + "meshery", + "microk8s", + "minikube", + "mirrord", + "ncp", + "nerdctl", + "nerdctl", //Docker-compatible CLI for containerd. + "newrelic", + "ngrok", + "nix", + "nomad", + "npm", + "openstack", + "otel", + "pack", //CLI tool maintained by the CNB project to support the use of buildpacks. + "packer", + "pip", + "pnpm", + "podman", + "poetry", + "pre-commit", + "promtool", + "pulumi", + "puppet", + "quarkus", + "rancher", + "rke", + "sam", + "sdkman", + "semgrep", + "serverless", + "sf", + "shellcheck", + "skaffold", + "sops", + "splunk", + "st2", + "stern", + "tanzu", + "telepresence", + "terraform", + "terraform-docs", + "terragrunt", + "terramate", + "tflint", + "tfsec", + "tfswitch", + "tilt", + "tofu", + "travis", + "trivy", + "uv", + "vagrant", + "vault", + "velero", + "werf", + "yamllint", + "yarn", + "yc", + "yq", + + "claude", + "codex", + "cursor", + "junie", + "opencode", + "copilot", + ) + + private val TOOL_DISCOVERED: EventId1 = GROUP.registerEvent( + "tool.discovered", + EventFields.String("tool", CLI_EXECUTABLES) + ) + + private val KUBECONFIG_ID: String = "kubeconfig" + + private val CLAUDE_ID: String = ".claude" + private val CODEX_ID: String = ".codex" + private val COPILOT_ID: String = ".copilot" + private val CURSOR_ID: String = ".cursor" + + private val CONFIG_EXISTS: EventId1 = GROUP.registerEvent( + "config.exists", + EventFields.String("config", listOf(KUBECONFIG_ID, CLAUDE_ID)) + ) + + override fun getGroup(): EventLogGroup = GROUP + + override fun getMetrics(): Set { + val pathNames = EnvironmentScanner.getPathNames() + val metrics = CLI_EXECUTABLES + .filter { EnvironmentScanner.hasToolInLocalPath(pathNames, it) } + .map { TOOL_DISCOVERED.metric(it) } + .toMutableList() + + if (userPathExists(Path.of(".kube", "config"))) { + metrics.add(CONFIG_EXISTS.metric(KUBECONFIG_ID)) + } + + if (userPathExists(Path.of(".claude"))) { + metrics.add(CONFIG_EXISTS.metric(CLAUDE_ID)) + } + if (userPathExists(Path.of(".codex"))) { + metrics.add(CONFIG_EXISTS.metric(CODEX_ID)) + } + if (userPathExists(Path.of(".copilot"))) { + metrics.add(CONFIG_EXISTS.metric(COPILOT_ID)) + } + if (userPathExists(Path.of(".cursor"))) { + metrics.add(CONFIG_EXISTS.metric(CURSOR_ID)) + } + + return metrics.toSet() + } + + private fun userPathExists(subPath: Path): Boolean { + val fs = FileSystems.getDefault() + val configPath = try { + fs.getPath(SystemProperties.getUserHome()).resolve(subPath) + } + catch (_: InvalidPathException) { + null + } + + return configPath != null && try { + Files.exists(configPath) + } + catch (_: Exception) { + false + } + } +} \ No newline at end of file