Skip to content

Commit 0c79959

Browse files
authored
fix(amazonq): use user provided node runtime (#5768)
1 parent dd2a1b6 commit 0c79959

File tree

4 files changed

+131
-1
lines changed

4 files changed

+131
-1
lines changed

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,21 @@ class CodeWhispererConfigurable(private val project: Project) :
8383
.resizableColumn()
8484
.align(Align.FILL)
8585
}
86+
row(message("amazonqFeatureDev.placeholder.node_runtime_path")) {
87+
val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileDescriptor()
88+
fileChooserDescriptor.isForcedToUseIdeaFileChooser = true
89+
90+
textFieldWithBrowseButton(fileChooserDescriptor = fileChooserDescriptor)
91+
.bindText(
92+
{ LspSettings.getInstance().getNodeRuntimePath().orEmpty() },
93+
{ LspSettings.getInstance().setNodeRuntimePath(it) }
94+
)
95+
.applyToComponent {
96+
emptyText.text = message("executableCommon.auto_managed")
97+
}
98+
.resizableColumn()
99+
.align(Align.FILL)
100+
}
86101
}
87102

88103
group(message("aws.settings.codewhisperer.group.general")) {

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp
55

66
import com.google.gson.ToNumberPolicy
77
import com.intellij.execution.configurations.GeneralCommandLine
8+
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
89
import com.intellij.execution.impl.ExecutionManagerImpl
910
import com.intellij.execution.process.KillableColoredProcessHandler
1011
import com.intellij.execution.process.KillableProcessHandler
1112
import com.intellij.execution.process.ProcessEvent
1213
import com.intellij.execution.process.ProcessListener
1314
import com.intellij.execution.process.ProcessOutputType
15+
import com.intellij.notification.NotificationAction
1416
import com.intellij.openapi.Disposable
1517
import com.intellij.openapi.components.Service
1618
import com.intellij.openapi.components.service
1719
import com.intellij.openapi.components.serviceIfCreated
20+
import com.intellij.openapi.options.ShowSettingsUtil
1821
import com.intellij.openapi.project.Project
1922
import com.intellij.openapi.util.Disposer
2023
import com.intellij.openapi.util.Key
@@ -76,6 +79,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceS
7679
import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig
7780
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
7881
import software.aws.toolkits.jetbrains.settings.LspSettings
82+
import software.aws.toolkits.jetbrains.utils.notifyInfo
83+
import software.aws.toolkits.resources.message
7984
import java.io.IOException
8085
import java.io.OutputStreamWriter
8186
import java.io.PipedInputStream
@@ -86,7 +91,9 @@ import java.net.Proxy
8691
import java.net.URI
8792
import java.nio.charset.StandardCharsets
8893
import java.nio.file.Files
94+
import java.nio.file.Path
8995
import java.util.concurrent.Future
96+
import java.util.concurrent.TimeUnit
9097
import kotlin.time.Duration.Companion.seconds
9198

9299
// https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java
@@ -372,7 +379,9 @@ private class AmazonQServerInstance(private val project: Project, private val cs
372379
}
373380

374381
val node = if (SystemInfo.isWindows) "node.exe" else "node"
375-
val cmd = NodeExePatcher.patch(artifact.resolve(node))
382+
var nodePath = getNodeRuntimePath(artifact.resolve(node))
383+
384+
val cmd = NodeExePatcher.patch(nodePath)
376385
.withParameters(
377386
LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(),
378387
"--stdio",
@@ -492,6 +501,103 @@ private class AmazonQServerInstance(private val project: Project, private val cs
492501
}
493502
}
494503

504+
/**
505+
* Resolves the path to a valid Node.js runtime in the following order of preference:
506+
* 1. Uses the provided nodePath if it exists and is executable
507+
* 2. Uses user-specified runtime path from LSP settings if available
508+
* 3. Uses system Node.js if version 18+ is available
509+
* 4. Falls back to original nodePath with a notification to configure runtime
510+
*
511+
* @param nodePath The initial Node.js runtime path to check, typically from the artifact directory
512+
* @return Path The resolved Node.js runtime path to use for the LSP server
513+
*
514+
* Side effects:
515+
* - Logs warnings if initial runtime path is invalid
516+
* - Logs info when using alternative runtime path
517+
* - Shows notification to user if no valid Node.js runtime is found
518+
*
519+
* Note: The function will return a path even if no valid runtime is found, but the LSP server
520+
* may fail to start in that case. The caller should handle potential runtime initialization failures.
521+
*/
522+
private fun getNodeRuntimePath(nodePath: Path): Path {
523+
if (Files.exists(nodePath) && Files.isExecutable(nodePath)) {
524+
return nodePath
525+
}
526+
// use alternative node runtime if it is not found
527+
LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " }
528+
// attempt to use user provided node runtime path
529+
val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath()
530+
if (!nodeRuntime.isNullOrEmpty()) {
531+
LOG.info { "Using node from $nodeRuntime " }
532+
return Path.of(nodeRuntime)
533+
} else {
534+
val localNode = locateNodeCommand()
535+
if (localNode != null) {
536+
LOG.info { "Using node from ${localNode.toAbsolutePath()}" }
537+
return localNode
538+
}
539+
notifyInfo(
540+
"Amazon Q",
541+
message("amazonqFeatureDev.placeholder.node_runtime_message"),
542+
project = project,
543+
listOf(
544+
NotificationAction.create(
545+
message("codewhisperer.actions.open_settings.title")
546+
) { _, notification ->
547+
ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title"))
548+
},
549+
NotificationAction.create(
550+
message("codewhisperer.notification.custom.simple.button.got_it")
551+
) { _, notification -> notification.expire() }
552+
)
553+
)
554+
return nodePath
555+
}
556+
}
557+
558+
/**
559+
* Locates node executable ≥18 in system PATH.
560+
* Uses IntelliJ's PathEnvironmentVariableUtil to find executables.
561+
*
562+
* @return Path? The absolute path to node ≥18 if found, null otherwise
563+
*/
564+
private fun locateNodeCommand(): Path? {
565+
val exeName = if (SystemInfo.isWindows) "node.exe" else "node"
566+
567+
return PathEnvironmentVariableUtil.findAllExeFilesInPath(exeName)
568+
.asSequence()
569+
.map { it.toPath() }
570+
.filter { Files.isRegularFile(it) && Files.isExecutable(it) }
571+
.firstNotNullOfOrNull { path ->
572+
try {
573+
val process = ProcessBuilder(path.toString(), "--version")
574+
.redirectErrorStream(true)
575+
.start()
576+
577+
if (!process.waitFor(5, TimeUnit.SECONDS)) {
578+
process.destroy()
579+
null
580+
} else if (process.exitValue() == 0) {
581+
val version = process.inputStream.bufferedReader().readText().trim()
582+
val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull()
583+
584+
if (majorVersion != null && majorVersion >= 18) {
585+
path.toAbsolutePath()
586+
} else {
587+
LOG.debug { "Node version < 18 found at: $path (version: $version)" }
588+
null
589+
}
590+
} else {
591+
LOG.debug { "Failed to get version from node at: $path" }
592+
null
593+
}
594+
} catch (e: Exception) {
595+
LOG.debug(e) { "Failed to check version for node at: $path" }
596+
null
597+
}
598+
}
599+
}
600+
495601
override fun dispose() {
496602
if (!launcherFuture.isDone) {
497603
try {

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,22 @@ class LspSettings : PersistentStateComponent<LspConfiguration> {
2525

2626
fun getArtifactPath() = state.artifactPath
2727

28+
fun getNodeRuntimePath() = state.nodeRuntimePath
29+
2830
fun setArtifactPath(artifactPath: String?) {
2931
state.artifactPath = artifactPath.nullize(nullizeSpaces = true)
3032
}
3133

34+
fun setNodeRuntimePath(nodeRuntimePath: String?) {
35+
state.nodeRuntimePath = nodeRuntimePath.nullize(nullizeSpaces = true)
36+
}
37+
3238
companion object {
3339
fun getInstance(): LspSettings = service()
3440
}
3541
}
3642

3743
class LspConfiguration : BaseState() {
3844
var artifactPath by string()
45+
var nodeRuntimePath by string()
3946
}

plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts=Downloadi
146146
amazonqFeatureDev.placeholder.generating_code=Generating code...
147147
amazonqFeatureDev.placeholder.lsp=LSP
148148
amazonqFeatureDev.placeholder.new_plan=Describe your task or issue in as much detail as possible
149+
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.
150+
amazonqFeatureDev.placeholder.node_runtime_path=Node Runtime Path
149151
amazonqFeatureDev.placeholder.provide_code_feedback=Provide feedback or comments
150152
amazonqFeatureDev.placeholder.select_lsp_artifact=Select LSP Artifact
151153
amazonqFeatureDev.placeholder.write_new_prompt=Write a new prompt

0 commit comments

Comments
 (0)