Skip to content

Commit 3a5ee17

Browse files
committed
fix(windows): avoid WSL bash in harness/tools
1 parent 8cbfe32 commit 3a5ee17

4 files changed

Lines changed: 173 additions & 46 deletions

File tree

crates/rexos-harness/src/lib.rs

Lines changed: 103 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use anyhow::{bail, Context};
66
const FEATURES_JSON: &str = "features.json";
77
const PROGRESS_MD: &str = "rexos-progress.md";
88
const INIT_SH: &str = "init.sh";
9+
const INIT_PS1: &str = "init.ps1";
910
const REXOS_DIR: &str = ".rexos";
1011
const SESSION_ID_FILE: &str = "session_id";
1112

@@ -16,8 +17,9 @@ pub fn init_workspace(workspace_dir: &Path) -> anyhow::Result<()> {
1617
let features_path = workspace_dir.join(FEATURES_JSON);
1718
let progress_path = workspace_dir.join(PROGRESS_MD);
1819
let init_sh_path = workspace_dir.join(INIT_SH);
20+
let init_ps1_path = workspace_dir.join(INIT_PS1);
1921

20-
if features_path.exists() || progress_path.exists() || init_sh_path.exists() {
22+
if features_path.exists() || progress_path.exists() || init_sh_path.exists() || init_ps1_path.exists() {
2123
bail!("workspace already initialized");
2224
}
2325

@@ -49,6 +51,15 @@ echo "[rexos] init.sh: customize this script for your project"
4951
)
5052
.with_context(|| format!("write {}", init_sh_path.display()))?;
5153

54+
std::fs::write(
55+
&init_ps1_path,
56+
r#"$ErrorActionPreference = "Stop"
57+
58+
Write-Output "[rexos] init.ps1: customize this script for your project"
59+
"#,
60+
)
61+
.with_context(|| format!("write {}", init_ps1_path.display()))?;
62+
5263
#[cfg(unix)]
5364
{
5465
use std::os::unix::fs::PermissionsExt;
@@ -58,7 +69,7 @@ echo "[rexos] init.sh: customize this script for your project"
5869
}
5970

6071
ensure_git_repo(workspace_dir)?;
61-
git(workspace_dir, ["add", FEATURES_JSON, PROGRESS_MD, INIT_SH])?;
72+
git(workspace_dir, ["add", FEATURES_JSON, PROGRESS_MD, INIT_SH, INIT_PS1])?;
6273
git_with_identity(
6374
workspace_dir,
6475
[
@@ -118,7 +129,7 @@ pub async fn bootstrap_with_prompt(
118129
)
119130
.await?;
120131

121-
run_init_sh(workspace_dir)?;
132+
run_init_script(workspace_dir)?;
122133
commit_checkpoint_if_dirty(workspace_dir, "chore: rexos harness bootstrap")?;
123134
Ok(())
124135
}
@@ -146,7 +157,7 @@ pub async fn run_harness(
146157
)
147158
.await?;
148159

149-
match run_init_sh_capture(workspace_dir) {
160+
match run_init_script_capture(workspace_dir) {
150161
Ok(_) => {
151162
commit_checkpoint_if_dirty(workspace_dir, "chore: rexos harness checkpoint")?;
152163
return Ok(out);
@@ -170,8 +181,12 @@ pub fn preflight(workspace_dir: &Path) -> anyhow::Result<()> {
170181
let features_path = workspace_dir.join(FEATURES_JSON);
171182
let progress_path = workspace_dir.join(PROGRESS_MD);
172183
let init_sh_path = workspace_dir.join(INIT_SH);
184+
let init_ps1_path = workspace_dir.join(INIT_PS1);
173185

174-
if !features_path.exists() || !progress_path.exists() || !init_sh_path.exists() {
186+
if !features_path.exists()
187+
|| !progress_path.exists()
188+
|| (!init_sh_path.exists() && !init_ps1_path.exists())
189+
{
175190
bail!(
176191
"workspace not initialized; run `rexos harness init {}`",
177192
workspace_dir.display()
@@ -212,14 +227,7 @@ pub fn preflight(workspace_dir: &Path) -> anyhow::Result<()> {
212227
println!("[rexos] features.json: could not parse (continuing)");
213228
}
214229

215-
let status = Command::new("bash")
216-
.arg(INIT_SH)
217-
.current_dir(workspace_dir)
218-
.status()
219-
.with_context(|| format!("run {}", init_sh_path.display()))?;
220-
if !status.success() {
221-
bail!("init.sh failed");
222-
}
230+
run_init_script(workspace_dir)?;
223231

224232
Ok(())
225233
}
@@ -336,7 +344,7 @@ Your job:
336344
Rules:
337345
- Work only inside the workspace directory.
338346
- Prefer tools (`fs_read`, `fs_write`, `shell`) to inspect and change files.
339-
- After edits, run `./init.sh` and ensure it succeeds.
347+
- After edits, run the workspace init script (`./init.sh`, or `./init.ps1` on Windows) and ensure it succeeds.
340348
- Commit your changes to git with a descriptive message.
341349
"#
342350
}
@@ -348,39 +356,101 @@ Rules:
348356
- Work only inside the workspace directory.
349357
- Make small, incremental progress (one feature at a time).
350358
- Prefer using tools (`fs_read`, `fs_write`, `shell`) to inspect and change files.
351-
- If you change code, run the workspace's `init.sh` (smoke checks) and fix any failures.
359+
- If you change code, run the workspace init script (smoke checks) and fix any failures.
352360
- Append a short summary to `rexos-progress.md`.
353361
- Commit meaningful progress to git with a descriptive message.
354362
"#
355363
}
356364

357-
fn run_init_sh(workspace_dir: &Path) -> anyhow::Result<()> {
358-
let init_sh_path = workspace_dir.join(INIT_SH);
359-
let status = Command::new("bash")
360-
.arg(INIT_SH)
361-
.current_dir(workspace_dir)
362-
.status()
363-
.with_context(|| format!("run {}", init_sh_path.display()))?;
364-
if !status.success() {
365-
bail!("init.sh failed");
365+
#[derive(Debug, Clone, Copy)]
366+
enum InitScript {
367+
Bash,
368+
PowerShell,
369+
}
370+
371+
fn select_init_script(workspace_dir: &Path) -> anyhow::Result<InitScript> {
372+
let sh_exists = workspace_dir.join(INIT_SH).exists();
373+
let ps1_exists = workspace_dir.join(INIT_PS1).exists();
374+
375+
if cfg!(windows) {
376+
if ps1_exists {
377+
return Ok(InitScript::PowerShell);
378+
}
379+
if sh_exists {
380+
return Ok(InitScript::Bash);
381+
}
382+
} else if sh_exists {
383+
return Ok(InitScript::Bash);
366384
}
367-
Ok(())
385+
386+
if ps1_exists && !sh_exists {
387+
bail!("init.ps1 exists but init.sh is missing");
388+
}
389+
390+
bail!("no init script found (expected init.sh and/or init.ps1)");
368391
}
369392

370-
fn run_init_sh_capture(workspace_dir: &Path) -> anyhow::Result<String> {
371-
let init_sh_path = workspace_dir.join(INIT_SH);
372-
let output = Command::new("bash")
373-
.arg(INIT_SH)
374-
.current_dir(workspace_dir)
375-
.output()
376-
.with_context(|| format!("run {}", init_sh_path.display()))?;
393+
fn run_init_script(workspace_dir: &Path) -> anyhow::Result<()> {
394+
match select_init_script(workspace_dir)? {
395+
InitScript::Bash => {
396+
let status = Command::new("bash")
397+
.arg(INIT_SH)
398+
.current_dir(workspace_dir)
399+
.status()
400+
.with_context(|| format!("run {}", workspace_dir.join(INIT_SH).display()))?;
401+
if !status.success() {
402+
bail!("init.sh failed");
403+
}
404+
Ok(())
405+
}
406+
InitScript::PowerShell => {
407+
let status = Command::new("powershell")
408+
.args([
409+
"-NoProfile",
410+
"-NonInteractive",
411+
"-ExecutionPolicy",
412+
"Bypass",
413+
"-File",
414+
INIT_PS1,
415+
])
416+
.current_dir(workspace_dir)
417+
.status()
418+
.with_context(|| format!("run {}", workspace_dir.join(INIT_PS1).display()))?;
419+
if !status.success() {
420+
bail!("init.ps1 failed");
421+
}
422+
Ok(())
423+
}
424+
}
425+
}
426+
427+
fn run_init_script_capture(workspace_dir: &Path) -> anyhow::Result<String> {
428+
let output = match select_init_script(workspace_dir)? {
429+
InitScript::Bash => Command::new("bash")
430+
.arg(INIT_SH)
431+
.current_dir(workspace_dir)
432+
.output()
433+
.with_context(|| format!("run {}", workspace_dir.join(INIT_SH).display()))?,
434+
InitScript::PowerShell => Command::new("powershell")
435+
.args([
436+
"-NoProfile",
437+
"-NonInteractive",
438+
"-ExecutionPolicy",
439+
"Bypass",
440+
"-File",
441+
INIT_PS1,
442+
])
443+
.current_dir(workspace_dir)
444+
.output()
445+
.with_context(|| format!("run {}", workspace_dir.join(INIT_PS1).display()))?,
446+
};
377447

378448
let mut combined = String::new();
379449
combined.push_str(&String::from_utf8_lossy(&output.stdout));
380450
combined.push_str(&String::from_utf8_lossy(&output.stderr));
381451

382452
if !output.status.success() {
383-
bail!("init.sh failed: {}", combined.trim());
453+
bail!("init failed: {}", combined.trim());
384454
}
385455

386456
Ok(combined)

crates/rexos-tools/src/lib.rs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,53 @@ impl Toolset {
9696

9797
let timeout = Duration::from_millis(timeout_ms.unwrap_or(60_000));
9898

99-
let mut cmd = tokio::process::Command::new("bash");
100-
cmd.arg("-c")
101-
.arg(command)
102-
.current_dir(&self.workspace_root)
103-
.env_clear()
104-
.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin");
99+
let mut cmd = if cfg!(windows) {
100+
let mut cmd = tokio::process::Command::new("powershell");
101+
cmd.args([
102+
"-NoProfile",
103+
"-NonInteractive",
104+
"-ExecutionPolicy",
105+
"Bypass",
106+
"-Command",
107+
]);
108+
109+
let wrapped = format!(
110+
"$ErrorActionPreference = 'Stop'; $global:LASTEXITCODE = 0; {command}; if ($global:LASTEXITCODE -ne 0) {{ exit $global:LASTEXITCODE }}",
111+
command = command
112+
);
113+
cmd.arg(wrapped);
114+
cmd
115+
} else {
116+
let mut cmd = tokio::process::Command::new("bash");
117+
cmd.arg("-c").arg(command);
118+
cmd
119+
};
120+
121+
cmd.current_dir(&self.workspace_root).env_clear();
122+
123+
if let Ok(path) = std::env::var("PATH") {
124+
cmd.env("PATH", path);
125+
}
126+
127+
if cfg!(windows) {
128+
for key in ["SystemRoot", "USERPROFILE", "TEMP", "TMP"] {
129+
if let Ok(v) = std::env::var(key) {
130+
cmd.env(key, v);
131+
}
132+
}
133+
} else {
134+
for key in ["HOME", "USER"] {
135+
if let Ok(v) = std::env::var(key) {
136+
cmd.env(key, v);
137+
}
138+
}
139+
}
140+
141+
for key in ["CARGO_HOME", "RUSTUP_HOME"] {
142+
if let Ok(v) = std::env::var(key) {
143+
cmd.env(key, v);
144+
}
145+
}
105146

106147
let output = tokio::time::timeout(timeout, cmd.output())
107148
.await
@@ -320,7 +361,7 @@ fn shell_def() -> ToolDefinition {
320361
kind: "function".to_string(),
321362
function: ToolFunctionDefinition {
322363
name: "shell".to_string(),
323-
description: "Run a shell command inside the workspace.".to_string(),
364+
description: "Run a shell command inside the workspace (bash on Unix, PowerShell on Windows).".to_string(),
324365
parameters: serde_json::json!({
325366
"type": "object",
326367
"properties": {

crates/rexos/tests/harness_runner.rs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ async fn harness_run_retries_on_init_sh_failure_and_checkpoints() {
1818
State(state): State<TestState>,
1919
Json(_payload): Json<serde_json::Value>,
2020
) -> Json<serde_json::Value> {
21+
let init_path = if cfg!(windows) { "init.ps1" } else { "init.sh" };
22+
let init_fail = if cfg!(windows) {
23+
"$ErrorActionPreference = \"Stop\"\nexit 1\n"
24+
} else {
25+
"#!/usr/bin/env bash\nexit 1\n"
26+
};
27+
let init_ok = if cfg!(windows) {
28+
"$ErrorActionPreference = \"Stop\"\nexit 0\n"
29+
} else {
30+
"#!/usr/bin/env bash\nexit 0\n"
31+
};
32+
2133
let mut calls = state.calls.lock().unwrap();
2234
*calls += 1;
2335

@@ -34,8 +46,8 @@ async fn harness_run_retries_on_init_sh_failure_and_checkpoints() {
3446
"function": {
3547
"name": "fs_write",
3648
"arguments": serde_json::to_string(&json!({
37-
"path": "init.sh",
38-
"content": "#!/usr/bin/env bash\nexit 1\n"
49+
"path": init_path,
50+
"content": init_fail
3951
})).unwrap()
4052
}
4153
}]
@@ -63,8 +75,8 @@ async fn harness_run_retries_on_init_sh_failure_and_checkpoints() {
6375
"function": {
6476
"name": "fs_write",
6577
"arguments": serde_json::to_string(&json!({
66-
"path": "init.sh",
67-
"content": "#!/usr/bin/env bash\nexit 0\n"
78+
"path": init_path,
79+
"content": init_ok
6880
})).unwrap()
6981
}
7082
},
@@ -168,4 +180,3 @@ async fn harness_run_retries_on_init_sh_failure_and_checkpoints() {
168180

169181
server.abort();
170182
}
171-

crates/rexos/tests/tools_sandbox.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,13 @@ async fn shell_tool_runs_in_workspace() {
3333
let root = tmp.path();
3434

3535
let tools = rexos::tools::Toolset::new(root.to_path_buf()).unwrap();
36+
let command = if cfg!(windows) {
37+
"(Get-Location).Path"
38+
} else {
39+
"pwd"
40+
};
3641
let out = tools
37-
.call("shell", r#"{ "command": "pwd" }"#)
42+
.call("shell", &format!(r#"{{ "command": "{command}" }}"#))
3843
.await
3944
.unwrap();
4045

0 commit comments

Comments
 (0)