Skip to content

Commit 33147c7

Browse files
committed
fix(shell): augment PATH with common tool directories on JVM
Ensure the shell executor prepends common Homebrew and Linuxbrew paths to PATH if not set in config. This improves tool discovery in production builds where the app does not inherit the user's full environment.
1 parent 7498602 commit 33147c7

File tree

3 files changed

+68
-19
lines changed

3 files changed

+68
-19
lines changed

mpp-core/src/jvmMain/kotlin/cc/unitmesh/agent/tool/shell/DefaultShellExecutor.jvm.kt

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ actual class DefaultShellExecutor : ShellExecutor {
6969
environment().putAll(config.environment)
7070
}
7171

72+
// Augment PATH to include common tool installation directories
73+
// This is critical for production builds where the app doesn't inherit
74+
// the user's full shell environment (e.g., Homebrew paths)
75+
augmentEnvironmentPath(environment(), config.environment)
76+
7277
// Redirect error stream if not inheriting IO
7378
if (!config.inheritIO) {
7479
redirectErrorStream(false)
@@ -191,6 +196,68 @@ actual class DefaultShellExecutor : ShellExecutor {
191196
}
192197
}
193198

199+
/**
200+
* Augment the PATH environment variable to include common tool installation directories.
201+
*
202+
* This is essential for production builds where the app runs as a standalone bundle
203+
* and doesn't inherit the user's full shell environment. Common package managers like
204+
* Homebrew install tools in non-standard paths that need to be explicitly added.
205+
*
206+
* @param processEnv The process environment map (will be modified)
207+
* @param configEnv The user-provided environment from config (should not override if PATH is set)
208+
*/
209+
private fun augmentEnvironmentPath(
210+
processEnv: MutableMap<String, String>,
211+
configEnv: Map<String, String>
212+
) {
213+
// Don't augment if user explicitly set PATH in config
214+
if (configEnv.containsKey("PATH")) {
215+
return
216+
}
217+
218+
val os = System.getProperty("os.name").lowercase()
219+
val currentPath = processEnv["PATH"] ?: System.getenv("PATH") ?: ""
220+
221+
// Determine additional paths based on OS
222+
val additionalPaths = when {
223+
os.contains("mac") || os.contains("darwin") -> listOf(
224+
"/opt/homebrew/bin", // Homebrew (Apple Silicon)
225+
"/opt/homebrew/sbin",
226+
"/usr/local/bin", // Homebrew (Intel)
227+
"/usr/local/sbin"
228+
)
229+
os.contains("linux") -> listOf(
230+
"/usr/local/bin",
231+
"/home/linuxbrew/.linuxbrew/bin", // Linuxbrew
232+
"/home/linuxbrew/.linuxbrew/sbin"
233+
)
234+
else -> emptyList() // Windows - use default PATH
235+
}
236+
237+
if (additionalPaths.isEmpty()) {
238+
return
239+
}
240+
241+
// Filter to only include paths that actually exist
242+
val existingAdditionalPaths = additionalPaths.filter { path ->
243+
File(path).exists()
244+
}
245+
246+
if (existingAdditionalPaths.isEmpty()) {
247+
return
248+
}
249+
250+
// Build the augmented PATH by prepending additional paths to current PATH
251+
// This ensures tools in these directories are found first
252+
val pathSeparator = if (os.contains("windows")) ";" else ":"
253+
val augmentedPath = (existingAdditionalPaths + currentPath.split(pathSeparator))
254+
.filter { it.isNotBlank() }
255+
.distinct()
256+
.joinToString(pathSeparator)
257+
258+
processEnv["PATH"] = augmentedPath
259+
}
260+
194261
private fun prepareCommand(command: String, shell: String?): List<String> {
195262
val effectiveShell = shell ?: getDefaultShell()
196263

mpp-ui/src/wasmJsMain/kotlin/cc/unitmesh/devins/ui/compose/sketch/MarkdownSketchRenderer.wasmJs.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,6 @@ actual object MarkdownSketchRenderer {
111111
mermaidCode = code,
112112
modifier = Modifier.fillMaxWidth().heightIn(min = 200.dp)
113113
)
114-
// RemoteMermaidRenderer(
115-
// mermaidCode = code,
116-
// modifier = Modifier.fillMaxWidth().heightIn(min = 200.dp)
117-
// )
118114
}
119115
}
120116
},

mpp-viewer-web/src/commonMain/kotlin/cc/unitmesh/viewer/web/MermaidRenderer.kt

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,10 @@ fun MermaidRenderer(
4747
modifier: Modifier = Modifier,
4848
onRenderComplete: ((success: Boolean, message: String) -> Unit)? = null
4949
) {
50-
// Track dynamic height from rendered content
5150
var webViewHeight by remember { mutableStateOf(200.dp) }
5251
var showFullscreen by remember { mutableStateOf(false) }
5352

5453
Box(modifier = modifier) {
55-
// Main WebView display
5654
MermaidWebView(
5755
mermaidCode = mermaidCode,
5856
isDarkTheme = isDarkTheme,
@@ -66,7 +64,6 @@ fun MermaidRenderer(
6664
onRenderComplete = onRenderComplete
6765
)
6866

69-
// Expand button (top-right corner)
7067
IconButton(
7168
onClick = { showFullscreen = true },
7269
modifier = Modifier
@@ -82,7 +79,6 @@ fun MermaidRenderer(
8279
}
8380
}
8481

85-
// Fullscreen dialog
8682
if (showFullscreen) {
8783
MermaidFullscreenDialog(
8884
mermaidCode = mermaidCode,
@@ -94,9 +90,7 @@ fun MermaidRenderer(
9490
}
9591
}
9692

97-
/**
98-
* Fullscreen Mermaid viewer with zoom controls
99-
*/
93+
10094
@Composable
10195
fun MermaidFullscreenDialog(
10296
mermaidCode: String,
@@ -120,7 +114,6 @@ fun MermaidFullscreenDialog(
120114
color = MaterialTheme.colorScheme.background
121115
) {
122116
Column(modifier = Modifier.fillMaxSize()) {
123-
// Top toolbar
124117
Row(
125118
modifier = Modifier
126119
.fillMaxWidth()
@@ -129,7 +122,6 @@ fun MermaidFullscreenDialog(
129122
horizontalArrangement = Arrangement.SpaceBetween,
130123
verticalAlignment = Alignment.CenterVertically
131124
) {
132-
// Zoom controls
133125
Row(
134126
horizontalArrangement = Arrangement.spacedBy(8.dp),
135127
verticalAlignment = Alignment.CenterVertically
@@ -168,7 +160,6 @@ fun MermaidFullscreenDialog(
168160
}
169161
}
170162

171-
// Close button
172163
IconButton(onClick = onDismiss) {
173164
Icon(
174165
imageVector = Icons.Default.Close,
@@ -194,9 +185,6 @@ fun MermaidFullscreenDialog(
194185
}
195186
}
196187

197-
/**
198-
* Internal WebView component for Mermaid rendering
199-
*/
200188
@Composable
201189
private fun MermaidWebView(
202190
mermaidCode: String,
@@ -213,7 +201,6 @@ private fun MermaidWebView(
213201
val jsBridge = rememberWebViewJsBridge()
214202

215203
LaunchedEffect(Unit) {
216-
// Handler for render callback
217204
jsBridge.register(object : IJsMessageHandler {
218205
override fun methodName(): String = "mermaidRenderCallback"
219206

@@ -227,7 +214,6 @@ private fun MermaidWebView(
227214
}
228215
})
229216

230-
// Handler for height updates
231217
jsBridge.register(object : IJsMessageHandler {
232218
override fun methodName(): String = "mermaidHeightCallback"
233219

0 commit comments

Comments
 (0)