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