From 5b449dc245a7b94743f72ef8052750ebe597af56 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Fri, 30 May 2025 12:35:01 -0700 Subject: [PATCH 1/8] fix: user provided node runtime --- .../settings/CodeWhispererConfigurable.kt | 15 ++++++++ .../services/amazonq/lsp/AmazonQLspService.kt | 36 ++++++++++++++++++- .../jetbrains/settings/LspSettings.kt | 7 ++++ .../resources/MessagesBundle.properties | 2 ++ 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt index e03521841da..4a8d97575a5 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt @@ -83,6 +83,21 @@ class CodeWhispererConfigurable(private val project: Project) : .resizableColumn() .align(Align.FILL) } + row(message("amazonqFeatureDev.placeholder.node_runtime_path")) { + val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileDescriptor() + fileChooserDescriptor.isForcedToUseIdeaFileChooser = true + + textFieldWithBrowseButton(fileChooserDescriptor = fileChooserDescriptor) + .bindText( + { LspSettings.getInstance().getNodeRuntimePath().orEmpty() }, + { LspSettings.getInstance().setNodeRuntimePath(it) } + ) + .applyToComponent { + emptyText.text = message("executableCommon.auto_managed") + } + .resizableColumn() + .align(Align.FILL) + } } group(message("aws.settings.codewhisperer.group.general")) { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index c2a0dd461dc..66f7c4ee118 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -11,10 +11,12 @@ import com.intellij.execution.process.KillableProcessHandler import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessListener import com.intellij.execution.process.ProcessOutputType +import com.intellij.notification.NotificationAction import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.components.serviceIfCreated +import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Key @@ -76,6 +78,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceS import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata import software.aws.toolkits.jetbrains.settings.LspSettings +import software.aws.toolkits.jetbrains.utils.notifyInfo +import software.aws.toolkits.resources.message import java.io.IOException import java.io.OutputStreamWriter import java.io.PipedInputStream @@ -84,6 +88,7 @@ import java.io.PrintWriter import java.io.StringWriter import java.net.Proxy import java.net.URI +import java.nio.file.Path import java.nio.charset.StandardCharsets import java.nio.file.Files import java.util.concurrent.Future @@ -372,7 +377,36 @@ private class AmazonQServerInstance(private val project: Project, private val cs } val node = if (SystemInfo.isWindows) "node.exe" else "node" - val cmd = NodeExePatcher.patch(artifact.resolve(node)) + var nodePath = artifact.resolve(node) + // download node runtime is not found + if (!Files.exists(nodePath) || !Files.isExecutable(nodePath)) { + LOG.warn() { "Node Runtime download failed. Fallback to user specified node runtime " } + // attempt to use user provided node runtime path + val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath() + if (!nodeRuntime.isNullOrEmpty()) { + nodePath = Path.of(nodeRuntime) + } else { + notifyInfo( + "Amazon Q", + message("amazonqFeatureDev.placeholder.node_runtime_message"), + project = project, + listOf( + NotificationAction.create( + message("codewhisperer.actions.open_settings.title") + ) { _, notification -> + ShowSettingsUtil.getInstance().showSettingsDialog(project, CodeWhispererConfigurable::class.java) + }, + NotificationAction.create( + message("codewhisperer.notification.custom.simple.button.got_it") + ) { _, notification -> notification.expire() } + ) + ) + + } + + } + + val cmd = NodeExePatcher.patch(nodePath) .withParameters( LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(), "--stdio", diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt index 61060774e79..bc199de4917 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt @@ -25,10 +25,16 @@ class LspSettings : PersistentStateComponent { fun getArtifactPath() = state.artifactPath + fun getNodeRuntimePath() = state.nodeRuntimePath + fun setArtifactPath(artifactPath: String?) { state.artifactPath = artifactPath.nullize(nullizeSpaces = true) } + fun setNodeRuntimePath(nodeRuntimePath: String?) { + state.nodeRuntimePath = nodeRuntimePath.nullize(nullizeSpaces = true) + } + companion object { fun getInstance(): LspSettings = service() } @@ -36,4 +42,5 @@ class LspSettings : PersistentStateComponent { class LspConfiguration : BaseState() { var artifactPath by string() + var nodeRuntimePath by string() } diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index d50132769c1..286a6b5d8a1 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -148,6 +148,8 @@ amazonqFeatureDev.placeholder.lsp=LSP amazonqFeatureDev.placeholder.new_plan=Describe your task or issue in as much detail as possible amazonqFeatureDev.placeholder.provide_code_feedback=Provide feedback or comments amazonqFeatureDev.placeholder.select_lsp_artifact=Select LSP Artifact +amazonqFeatureDev.placeholder.node_runtime_path=Node Runtime Path +amazonqFeatureDev.placeholder.node_runtime_message=Absolute path of your node js v18+ runtime executable. amazonqFeatureDev.placeholder.write_new_prompt=Write a new prompt apprunner.action.configure=Configure Service apprunner.action.create.service=Create Service... From 08255c6d2614a81ccdf789ee99a9863133ad08ea Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Fri, 30 May 2025 13:04:15 -0700 Subject: [PATCH 2/8] node --- .../jetbrains/services/amazonq/lsp/AmazonQLspService.kt | 6 +++--- .../aws/toolkits/resources/MessagesBundle.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index 66f7c4ee118..c3c3072aca8 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -378,9 +378,9 @@ private class AmazonQServerInstance(private val project: Project, private val cs val node = if (SystemInfo.isWindows) "node.exe" else "node" var nodePath = artifact.resolve(node) - // download node runtime is not found + // download node runtime if it is not found if (!Files.exists(nodePath) || !Files.isExecutable(nodePath)) { - LOG.warn() { "Node Runtime download failed. Fallback to user specified node runtime " } + LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " } // attempt to use user provided node runtime path val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath() if (!nodeRuntime.isNullOrEmpty()) { @@ -394,7 +394,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs NotificationAction.create( message("codewhisperer.actions.open_settings.title") ) { _, notification -> - ShowSettingsUtil.getInstance().showSettingsDialog(project, CodeWhispererConfigurable::class.java) + ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title")) }, NotificationAction.create( message("codewhisperer.notification.custom.simple.button.got_it") diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index 286a6b5d8a1..5059bcc0ab4 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -149,7 +149,7 @@ amazonqFeatureDev.placeholder.new_plan=Describe your task or issue in as much de amazonqFeatureDev.placeholder.provide_code_feedback=Provide feedback or comments amazonqFeatureDev.placeholder.select_lsp_artifact=Select LSP Artifact amazonqFeatureDev.placeholder.node_runtime_path=Node Runtime Path -amazonqFeatureDev.placeholder.node_runtime_message=Absolute path of your node js v18+ runtime executable. +amazonqFeatureDev.placeholder.node_runtime_message=Please provide the absolute path of your node js v18+ runtime executable in Settings. Re-open IDE to apply this change. amazonqFeatureDev.placeholder.write_new_prompt=Write a new prompt apprunner.action.configure=Configure Service apprunner.action.create.service=Create Service... From 2e07596294f8b0ebc889ae5885a481fba29b6965 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Fri, 30 May 2025 13:48:01 -0700 Subject: [PATCH 3/8] fix detekt --- .../jetbrains/services/amazonq/lsp/AmazonQLspService.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index c3c3072aca8..6a76a211e93 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -88,9 +88,9 @@ import java.io.PrintWriter import java.io.StringWriter import java.net.Proxy import java.net.URI -import java.nio.file.Path import java.nio.charset.StandardCharsets import java.nio.file.Files +import java.nio.file.Path import java.util.concurrent.Future import kotlin.time.Duration.Companion.seconds @@ -401,9 +401,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs ) { _, notification -> notification.expire() } ) ) - } - } val cmd = NodeExePatcher.patch(nodePath) From d0b5a1b00c81c84dac3b4cfc57aafe914e7bead4 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Fri, 30 May 2025 16:46:06 -0700 Subject: [PATCH 4/8] get node from env --- .../services/amazonq/lsp/AmazonQLspService.kt | 147 ++++++++++++++---- .../amazonq/lsp/artifacts/ArtifactHelper.kt | 1 + 2 files changed, 122 insertions(+), 26 deletions(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index 6a76a211e93..fdb390a1937 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -92,6 +92,7 @@ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path import java.util.concurrent.Future +import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.seconds // https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java @@ -377,32 +378,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs } val node = if (SystemInfo.isWindows) "node.exe" else "node" - var nodePath = artifact.resolve(node) - // download node runtime if it is not found - if (!Files.exists(nodePath) || !Files.isExecutable(nodePath)) { - LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " } - // attempt to use user provided node runtime path - val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath() - if (!nodeRuntime.isNullOrEmpty()) { - nodePath = Path.of(nodeRuntime) - } else { - notifyInfo( - "Amazon Q", - message("amazonqFeatureDev.placeholder.node_runtime_message"), - project = project, - listOf( - NotificationAction.create( - message("codewhisperer.actions.open_settings.title") - ) { _, notification -> - ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title")) - }, - NotificationAction.create( - message("codewhisperer.notification.custom.simple.button.got_it") - ) { _, notification -> notification.expire() } - ) - ) - } - } + var nodePath = getNodeRuntimePath(artifact.resolve(node)) val cmd = NodeExePatcher.patch(nodePath) .withParameters( @@ -524,6 +500,125 @@ private class AmazonQServerInstance(private val project: Project, private val cs } } + /** + * Resolves the path to a valid Node.js runtime in the following order of preference: + * 1. Uses the provided nodePath if it exists and is executable + * 2. Uses user-specified runtime path from LSP settings if available + * 3. Uses system Node.js if version 18+ is available + * 4. Falls back to original nodePath with a notification to configure runtime + * + * @param nodePath The initial Node.js runtime path to check, typically from the artifact directory + * @return Path The resolved Node.js runtime path to use for the LSP server + * + * Side effects: + * - Logs warnings if initial runtime path is invalid + * - Logs info when using alternative runtime path + * - Shows notification to user if no valid Node.js runtime is found + * + * Note: The function will return a path even if no valid runtime is found, but the LSP server + * may fail to start in that case. The caller should handle potential runtime initialization failures. + */ + private fun getNodeRuntimePath(nodePath: Path): Path { + if (Files.exists(nodePath) && Files.isExecutable(nodePath)) { + return nodePath + } + // use alternative node runtime if it is not found + LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " } + // attempt to use user provided node runtime path + val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath() + if (!nodeRuntime.isNullOrEmpty()) { + LOG.info { "Using node from $nodeRuntime " } + return Path.of(nodeRuntime) + } else { + val localNode = locateNodeCommand() + if (localNode != null) { + LOG.info { "Using node from ${localNode.toAbsolutePath()}" } + return localNode + } + notifyInfo( + "Amazon Q", + message("amazonqFeatureDev.placeholder.node_runtime_message"), + project = project, + listOf( + NotificationAction.create( + message("codewhisperer.actions.open_settings.title") + ) { _, notification -> + ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title")) + }, + NotificationAction.create( + message("codewhisperer.notification.custom.simple.button.got_it") + ) { _, notification -> notification.expire() } + ) + ) + return nodePath + } + } + + /** + * Locates node executable using platform-specific command. + * Uses 'where' on Windows and 'which' on Unix-like systems. + * Only gets node newer than node 18! + * @return Path? The absolute path to node if found via where/which, null otherwise + */ + private fun locateNodeCommand(): Path? { + val command = if (SystemInfo.isWindows) { + arrayOf("where", "node.exe") + } else { + arrayOf("which", "node") + } + + return try { + val process = ProcessBuilder(*command) + .redirectErrorStream(true) + .start() + + if (!process.waitFor(2, TimeUnit.SECONDS)) { + process.destroy() + return null + } + + if (process.exitValue() != 0) { + return null + } + + // where/which can return multiple lines - check each path until we find node ≥18 + process.inputStream.bufferedReader() + .lineSequence() + .map { Path.of(it.trim()) } + .filter { Files.isRegularFile(it) && Files.isExecutable(it) } + .firstNotNullOfOrNull { path -> + // Check version for each found node + val versionProcess = ProcessBuilder(path.toString(), "--version") + .redirectErrorStream(true) + .start() + + try { + if (!versionProcess.waitFor(2, TimeUnit.SECONDS)) { + versionProcess.destroy() + null + } else if (versionProcess.exitValue() == 0) { + val version = versionProcess.inputStream.bufferedReader().readText().trim() + val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull() + + if (majorVersion != null && majorVersion >= 18) { + path.toAbsolutePath() + } else { + null + } + } else { + null + } + } catch (e: Exception) { + LOG.debug(e) { "Failed to check version for node at: $path" } + null + } + } + } catch (e: Exception) { + LOG.debug(e) { "Failed to locate node using ${command.joinToString(" ")}" } + null + } + } + override fun dispose() { if (!launcherFuture.isDone) { try { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt index 97f2db11fed..a03d47201ca 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt @@ -114,6 +114,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, logger.info { "Attempt ${currentAttempt.get()} of $maxDownloadAttempts to download LSP artifacts" } try { + throw Error("ee") return withBackgroundProgress( project, AwsCoreBundle.message("amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts"), From d2cefe22ef70f6929664b1b524b3b4e28fc857b5 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Fri, 30 May 2025 16:48:07 -0700 Subject: [PATCH 5/8] no throw --- .../jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt index a03d47201ca..97f2db11fed 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt @@ -114,7 +114,6 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, logger.info { "Attempt ${currentAttempt.get()} of $maxDownloadAttempts to download LSP artifacts" } try { - throw Error("ee") return withBackgroundProgress( project, AwsCoreBundle.message("amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts"), From 201976d1f20c6b8d2ccec37234d6afa08b8ded76 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Fri, 30 May 2025 16:57:59 -0700 Subject: [PATCH 6/8] sort messages --- .../software/aws/toolkits/resources/MessagesBundle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index 5059bcc0ab4..0871d6258f5 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -146,10 +146,10 @@ amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts=Downloadi amazonqFeatureDev.placeholder.generating_code=Generating code... amazonqFeatureDev.placeholder.lsp=LSP amazonqFeatureDev.placeholder.new_plan=Describe your task or issue in as much detail as possible +amazonqFeatureDev.placeholder.node_runtime_message=Please provide the absolute path of your node js v18+ runtime executable in Settings. Re-open IDE to apply this change. +amazonqFeatureDev.placeholder.node_runtime_path=Node Runtime Path amazonqFeatureDev.placeholder.provide_code_feedback=Provide feedback or comments amazonqFeatureDev.placeholder.select_lsp_artifact=Select LSP Artifact -amazonqFeatureDev.placeholder.node_runtime_path=Node Runtime Path -amazonqFeatureDev.placeholder.node_runtime_message=Please provide the absolute path of your node js v18+ runtime executable in Settings. Re-open IDE to apply this change. amazonqFeatureDev.placeholder.write_new_prompt=Write a new prompt apprunner.action.configure=Configure Service apprunner.action.create.service=Create Service... From 29cd89002c2a0d8d4b9b54d71731f79da55fd508 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Fri, 30 May 2025 17:42:45 -0700 Subject: [PATCH 7/8] use PathEnvironmentVariableUtil --- .../services/amazonq/lsp/AmazonQLspService.kt | 81 +++++++------------ 1 file changed, 30 insertions(+), 51 deletions(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index fdb390a1937..711595a2a60 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp import com.google.gson.ToNumberPolicy import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.configurations.PathEnvironmentVariableUtil import com.intellij.execution.impl.ExecutionManagerImpl import com.intellij.execution.process.KillableColoredProcessHandler import com.intellij.execution.process.KillableProcessHandler @@ -555,68 +556,46 @@ private class AmazonQServerInstance(private val project: Project, private val cs } /** - * Locates node executable using platform-specific command. - * Uses 'where' on Windows and 'which' on Unix-like systems. - * Only gets node newer than node 18! - * @return Path? The absolute path to node if found via where/which, null otherwise + * Locates node executable ≥18 in system PATH. + * Uses IntelliJ's PathEnvironmentVariableUtil to find executables. + * + * @return Path? The absolute path to node ≥18 if found, null otherwise */ private fun locateNodeCommand(): Path? { - val command = if (SystemInfo.isWindows) { - arrayOf("where", "node.exe") - } else { - arrayOf("which", "node") - } - - return try { - val process = ProcessBuilder(*command) - .redirectErrorStream(true) - .start() - - if (!process.waitFor(2, TimeUnit.SECONDS)) { - process.destroy() - return null - } - - if (process.exitValue() != 0) { - return null - } - - // where/which can return multiple lines - check each path until we find node ≥18 - process.inputStream.bufferedReader() - .lineSequence() - .map { Path.of(it.trim()) } - .filter { Files.isRegularFile(it) && Files.isExecutable(it) } - .firstNotNullOfOrNull { path -> - // Check version for each found node - val versionProcess = ProcessBuilder(path.toString(), "--version") + val exeName = if (SystemInfo.isWindows) "node.exe" else "node" + + return PathEnvironmentVariableUtil.findAllExeFilesInPath(exeName) + .asSequence() + .map { it.toPath() } + .filter { Files.isRegularFile(it) && Files.isExecutable(it) } + .firstNotNullOfOrNull { path -> + try { + val process = ProcessBuilder(path.toString(), "--version") .redirectErrorStream(true) .start() - try { - if (!versionProcess.waitFor(2, TimeUnit.SECONDS)) { - versionProcess.destroy() - null - } else if (versionProcess.exitValue() == 0) { - val version = versionProcess.inputStream.bufferedReader().readText().trim() - val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull() - - if (majorVersion != null && majorVersion >= 18) { - path.toAbsolutePath() - } else { - null - } + if (!process.waitFor(2, TimeUnit.SECONDS)) { + process.destroy() + null + } else if (process.exitValue() == 0) { + val version = process.inputStream.bufferedReader().readText().trim() + val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull() + + if (majorVersion != null && majorVersion >= 18) { + path.toAbsolutePath() } else { + LOG.debug { "Node version < 18 found at: $path (version: $version)" } null } - } catch (e: Exception) { - LOG.debug(e) { "Failed to check version for node at: $path" } + } else { + LOG.debug { "Failed to get version from node at: $path" } null } + } catch (e: Exception) { + LOG.debug(e) { "Failed to check version for node at: $path" } + null } - } catch (e: Exception) { - LOG.debug(e) { "Failed to locate node using ${command.joinToString(" ")}" } - null - } + } } override fun dispose() { From 2d81a61907478f69fb87405984ea7fd2a5fc68c9 Mon Sep 17 00:00:00 2001 From: Lei Gao Date: Mon, 2 Jun 2025 08:52:25 -0700 Subject: [PATCH 8/8] 5 sec start --- .../jetbrains/services/amazonq/lsp/AmazonQLspService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index 711595a2a60..cb9dd485504 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -574,7 +574,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs .redirectErrorStream(true) .start() - if (!process.waitFor(2, TimeUnit.SECONDS)) { + if (!process.waitFor(5, TimeUnit.SECONDS)) { process.destroy() null } else if (process.exitValue() == 0) {