Skip to content

Commit 8bed832

Browse files
committed
fix: NVM based OpenCode installs not working
1 parent a70d246 commit 8bed832

5 files changed

Lines changed: 104 additions & 8 deletions

File tree

src/main/kotlin/com/ashotn/opencode/relay/OpenCodeChecker.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,17 @@ object OpenCodeChecker {
4343
add("/usr/local/bin/opencode") // Homebrew (Intel Mac)
4444
add("/opt/homebrew/bin/opencode") // Homebrew (Apple Silicon)
4545
home?.let {
46+
addAll(nvmInstallLocations("$it/.nvm"))
4647
add("$it/.local/bin/opencode")
4748
add("$it/.bun/bin/opencode")
4849
add("$it/.npm-global/bin/opencode")
4950
}
51+
System.getenv("NVM_BIN")?.takeIf { it.isNotBlank() }?.let {
52+
add(File(it, "opencode").path)
53+
}
54+
System.getenv("NVM_DIR")?.takeIf { it.isNotBlank() }?.let {
55+
addAll(nvmInstallLocations(it))
56+
}
5057
}
5158
}
5259

@@ -55,11 +62,18 @@ object OpenCodeChecker {
5562
buildList {
5663
add("/usr/bin/opencode")
5764
home?.let {
65+
addAll(nvmInstallLocations("$it/.nvm"))
5866
add("$it/.opencode/bin/opencode")
5967
add("$it/.local/bin/opencode")
6068
add("$it/.bun/bin/opencode")
6169
add("$it/.npm-global/bin/opencode")
6270
}
71+
System.getenv("NVM_BIN")?.takeIf { it.isNotBlank() }?.let {
72+
add(File(it, "opencode").path)
73+
}
74+
System.getenv("NVM_DIR")?.takeIf { it.isNotBlank() }?.let {
75+
addAll(nvmInstallLocations(it))
76+
}
6377
}
6478
}
6579

@@ -97,6 +111,22 @@ object OpenCodeChecker {
97111
}
98112
}
99113

114+
// NVM keeps packages under each installed Node version, so scan versions/node/*/bin/opencode.
115+
private fun nvmInstallLocations(rootPath: String): List<String> {
116+
val root = File(rootPath)
117+
val versionsRoot = File(root, "versions/node")
118+
if (!versionsRoot.isDirectory) return emptyList()
119+
120+
return versionsRoot.listFiles()
121+
.orEmpty()
122+
.asSequence()
123+
.filter { it.isDirectory }
124+
.sortedByDescending { it.name }
125+
.map { versionDir -> File(versionDir, "bin/opencode").path }
126+
.distinct()
127+
.toList()
128+
}
129+
100130
private fun autoResolve(): OpenCodeInfo? {
101131
val executableNames =
102132
if (SystemInfo.isWindows) {
@@ -212,6 +242,7 @@ object OpenCodeChecker {
212242
return try {
213243
val process = ProcessBuilder(path, arg)
214244
.redirectErrorStream(true)
245+
.apply { OpenCodeProcessEnvironment.configure(this, path) }
215246
.start()
216247

217248
var output = ""
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.ashotn.opencode.relay
2+
3+
import com.intellij.openapi.util.SystemInfo
4+
import java.io.File
5+
6+
internal object OpenCodeProcessEnvironment {
7+
8+
fun configure(processBuilder: ProcessBuilder, executablePath: String) {
9+
nvmBinDirectory(executablePath)?.let { binDir ->
10+
prependPath(processBuilder.environment(), binDir)
11+
}
12+
}
13+
14+
fun terminalCommand(command: List<String>): List<String> {
15+
if (command.isEmpty()) return command
16+
17+
val executablePath = command.first()
18+
val nvmBinDirectory = nvmBinDirectory(executablePath) ?: return command
19+
if (SystemInfo.isWindows) return command
20+
21+
return listOf(
22+
"/usr/bin/env",
23+
"PATH=${pathWithPrependedDirectory(System.getenv("PATH"), nvmBinDirectory)}",
24+
) + command
25+
}
26+
27+
private fun nvmBinDirectory(executablePath: String): String? {
28+
val executable = File(executablePath)
29+
val parent = executable.parentFile ?: return null
30+
val normalizedParent = parent.invariantSeparatorsPath
31+
val normalizedExecutable = executable.invariantSeparatorsPath
32+
33+
return if (
34+
normalizedParent.endsWith("/bin") &&
35+
normalizedExecutable.contains("/.nvm/versions/node/")
36+
) {
37+
parent.absolutePath
38+
} else {
39+
null
40+
}
41+
}
42+
43+
private fun prependPath(environment: MutableMap<String, String>, directory: String) {
44+
val pathKey = environment.keys.firstOrNull { it.equals("PATH", ignoreCase = true) } ?: "PATH"
45+
environment[pathKey] = pathWithPrependedDirectory(environment[pathKey], directory)
46+
}
47+
48+
private fun pathWithPrependedDirectory(currentPath: String?, directory: String): String {
49+
val pathEntries = currentPath.orEmpty().split(File.pathSeparator).filter { it.isNotBlank() }
50+
if (pathEntries.any { it == directory }) return currentPath.orEmpty()
51+
52+
return if (currentPath.isNullOrBlank()) {
53+
directory
54+
} else {
55+
directory + File.pathSeparator + currentPath
56+
}
57+
}
58+
}

src/main/kotlin/com/ashotn/opencode/relay/ServerManager.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ class ServerManager(
305305
val process = ProcessBuilder(command)
306306
.inheritIO()
307307
.apply {
308+
OpenCodeProcessEnvironment.configure(this, executablePath)
308309
val basePath = project.basePath
309310
if (basePath != null) directory(File(basePath))
310311
}

src/main/kotlin/com/ashotn/opencode/relay/terminal/ClassicTuiPanel.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.ashotn.opencode.relay.terminal
22

33
import com.ashotn.opencode.relay.OpenCodePlugin
4+
import com.ashotn.opencode.relay.OpenCodeProcessEnvironment
45
import com.ashotn.opencode.relay.settings.OpenCodeSettings
56
import com.ashotn.opencode.relay.util.serverUrl
67
import com.intellij.openapi.Disposable
@@ -64,10 +65,12 @@ class ClassicTuiPanel(
6465
}
6566

6667
val workingDir = project.basePath ?: System.getProperty("user.home")
67-
val command = listOf(
68-
executablePath,
69-
"attach",
70-
serverUrl(OpenCodeSettings.getInstance(project).serverPort),
68+
val command = OpenCodeProcessEnvironment.terminalCommand(
69+
listOf(
70+
executablePath,
71+
"attach",
72+
serverUrl(OpenCodeSettings.getInstance(project).serverPort),
73+
)
7174
)
7275

7376
val runner = LocalTerminalDirectRunner.createTerminalRunner(project)

src/main/kotlin/com/ashotn/opencode/relay/terminal/ReworkedTuiPanel.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package com.ashotn.opencode.relay.terminal
44

55
import com.ashotn.opencode.relay.OpenCodePlugin
6+
import com.ashotn.opencode.relay.OpenCodeProcessEnvironment
67
import com.ashotn.opencode.relay.settings.OpenCodeSettings
78
import com.ashotn.opencode.relay.util.serverUrl
89
import com.intellij.openapi.Disposable
@@ -71,10 +72,12 @@ class ReworkedTuiPanel(
7172
}
7273

7374
val workingDir = project.basePath ?: System.getProperty("user.home")
74-
val command = listOf(
75-
executablePath,
76-
"attach",
77-
serverUrl(OpenCodeSettings.getInstance(project).serverPort),
75+
val command = OpenCodeProcessEnvironment.terminalCommand(
76+
listOf(
77+
executablePath,
78+
"attach",
79+
serverUrl(OpenCodeSettings.getInstance(project).serverPort),
80+
)
7881
)
7982
val manager = TerminalToolWindowTabsManager.getInstance(project)
8083

0 commit comments

Comments
 (0)