Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ libc = "0.2"
# Win32 calls std lacks: run liveness, console ownership, the error dialog, PE
# version-resource reads (GetFileVersionInfoW / VerQueryValueW — the spawn-free
# `--version` fast path in harness detection), total RAM for the agent idle
# policy, and the desktop app's single instance and taskbar identity. Already in
# the tree.
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Credentials", "Win32_Storage_FileSystem", "Win32_System_Diagnostics_ToolHelp", "Win32_System_JobObjects", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Console", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
# policy, the desktop app's single instance and taskbar identity, and the
# registry PATH a running orx re-reads. Already in the tree.
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Credentials", "Win32_Storage_FileSystem", "Win32_System_Diagnostics_ToolHelp", "Win32_System_JobObjects", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Console", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }

[target.'cfg(any(target_os = "macos", windows))'.dependencies]
# Desktop app mode (src/commands/app.rs): the dashboard in its own window. tao
Expand Down
17 changes: 13 additions & 4 deletions docs/windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@ experiment runs and the nanochat demo. The gaps are listed at the bottom.
**Git for Windows is required**, and for more than git. It is the only source of
the `bash` and coreutils that orx uses to run experiments — the `bash.exe` in
`System32` is the WSL launcher, which cannot see your files, and orx rejects it.
Install it with the standard installer so `git.exe` lands on `PATH`; orx finds
the shell by walking up from there.

You also need a coding agent. Claude Code is the default:
Install it with the standard installer so `git.exe` lands on `PATH`, or let
onboarding install it (below); orx finds the shell by walking up from `git.exe`.
orx notices a Git installed while it runs, so **Re-check** in onboarding is
enough; there is no need to restart.

If you have no Git, onboarding offers **Install Git**. orx downloads the latest
PortableGit from Git for Windows' GitHub releases, checks it against the SHA-256
GitHub publishes, and unpacks it into `%LOCALAPPDATA%\OpenResearch\PortableGit`
with no administrator prompt. orx, its agents, and its terminals use it; your
own `PATH` is unchanged, and a Git you install later takes precedence.

You also need a coding agent. Claude Code is the default. To install Git, Node,
and Claude Code yourself:

```powershell
winget install --id Git.Git -e
Expand Down
16 changes: 16 additions & 0 deletions src/commands/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ fn router(state: AppState, remote_auth: Option<RemoteAuth>) -> Router {
.route("/api/onboarding/complete", post(complete_onboarding))
.route("/api/project-path/status", get(project_path_status))
.route("/api/project-path/pick", post(pick_project_folder))
.route("/api/git/install", post(install_git))
.route("/api/projects", get(list_projects).post(create_project))
.route(
"/api/projects/starter-prompts/prewarm",
Expand Down Expand Up @@ -837,6 +838,7 @@ fn remote_route_forbidden(path: &str) -> bool {
matches!(
path,
"/api/project-path/pick"
| "/api/git/install"
| "/api/update"
| "/api/update/apply"
| "/api/update/restart"
Expand Down Expand Up @@ -1357,12 +1359,26 @@ struct ProjectPathStatusQ {
path: Option<String>,
}

async fn install_git() -> ApiResult {
// Spawned, so a page reload mid-install cannot orphan the extractor.
tokio::spawn(local::portable_git::install())
.await
.map_err(|error| ApiError::from(anyhow!("Git install task failed: {error}")))?
// `:#` keeps the cause (DNS, TLS, proxy) behind the outer context.
.map_err(|error| {
eprintln!("orx up: Git install failed: {error:#}");
ApiError(StatusCode::INTERNAL_SERVER_ERROR, format!("{error:#}"))
})?;
Ok(Json(json!({})))
}

async fn project_path_status(Query(q): Query<ProjectPathStatusQ>) -> ApiResult {
tokio::task::spawn_blocking(move || -> Result<Json<Value>> {
let git_version = local::git::version();
let Some(path) = q.path.filter(|path| !path.trim().is_empty()) else {
return Ok(Json(json!({
"gitVersion": git_version,
"gitInstallable": cfg!(windows) && git_version.is_none(),
"resolvedPath": null,
"exists": null,
"directory": null,
Expand Down
44 changes: 38 additions & 6 deletions src/commands/up/harness_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,23 @@ fn windows_powershell() -> String {
"powershell.exe".into()
}

/// `$LASTEXITCODE` persists from an earlier native call, so reset it or a
/// failing cmdlet exits with its code.
#[cfg(any(windows, test))]
fn windows_install_script(install: &str) -> String {
format!(
"$LASTEXITCODE = 0\n{install}\nif (-not $?) {{ if ($LASTEXITCODE) {{ exit $LASTEXITCODE }} else {{ exit 1 }} }}"
)
}

/// PowerShell's `-EncodedCommand` form: base64 of the script's UTF-16LE.
#[cfg(any(windows, test))]
fn encoded_command(script: &str) -> String {
use base64::Engine;
let bytes: Vec<u8> = script.encode_utf16().flat_map(u16::to_le_bytes).collect();
base64::engine::general_purpose::STANDARD.encode(bytes)
}

fn login_command(harness: &str) -> Option<(&'static str, Vec<String>)> {
match harness {
"claude-code" => Some(("claude auth login", vec!["auth".into(), "login".into()])),
Expand Down Expand Up @@ -278,12 +295,10 @@ async fn run(
windows_powershell(),
vec![
"-NoProfile".into(),
"-Command".into(),
// `$LASTEXITCODE` persists from an earlier native call,
// so reset it or a failing cmdlet exits with its code.
format!(
"$LASTEXITCODE = 0\n{install}\nif (-not $?) {{ if ($LASTEXITCODE) {{ exit $LASTEXITCODE }} else {{ exit 1 }} }}"
),
// Encoded: the console title echoes the command line into
// the output, where the script's own phrases would be excerpted.
"-EncodedCommand".into(),
encoded_command(&windows_install_script(&install)),
],
)
}
Expand Down Expand Up @@ -778,6 +793,23 @@ mod tests {
assert!(!fresh_install(Action::Install, false, None));
}

#[test]
fn the_windows_install_command_line_carries_none_of_its_phrases() {
let script = windows_install_script(windows_install_command("opencode").unwrap());
assert!(script.contains("GitHub download failed"));
// Nothing the script prints on failure may quote its own text either.
assert!(!script.contains("Write-Error"));
let encoded = encoded_command(&script);
assert_eq!(crate::telemetry::harness::excerpt_for_test(&encoded), None);
assert_eq!(
encoded_command("exit 1"),
base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
b"e\0x\0i\0t\0 \x001\0"
)
);
}

#[test]
fn spawn_failure_keeps_the_os_cause_and_drops_the_script() {
// The whole install script is in the error; its unexecuted
Expand Down
51 changes: 42 additions & 9 deletions src/commands/up/install_opencode.ps1
Original file line number Diff line number Diff line change
@@ -1,27 +1,60 @@
$ErrorActionPreference = 'Stop'
# Windows PowerShell's progress bar slows Invoke-WebRequest to a crawl.
$ProgressPreference = 'SilentlyContinue'
# Windows PowerShell 5.1 can default to TLS 1.0, which GitHub and npm refuse.
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
$arch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
# ponytail: baseline supports all x64 CPUs; select AVX2 builds if performance requires it.
$asset = switch ($arch) {
'X64' { 'x64-baseline' }
'Arm64' { 'arm64' }
default { throw "Unsupported architecture: $arch" }
default { throw "OpenCode has no Windows build for $arch processors." }
}
$temp = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
$destination = Join-Path $env:USERPROFILE '.opencode\bin'
$installed = Join-Path $destination 'opencode.exe'
$release = "https://github.com/anomalyco/opencode/releases/latest/download/opencode-windows-$asset.zip"
New-Item -ItemType Directory -Path $temp -Force | Out-Null
try {
Write-Output 'Downloading OpenCode...'
$archive = Join-Path $temp 'opencode.zip'
curl.exe -fL --retry 2 --connect-timeout 15 --max-time 300 "https://github.com/anomalyco/opencode/releases/latest/download/opencode-windows-$asset.zip" -o $archive
# Keep curl's own exit code so distinct network faults stay apart; the
# throws below surface as exit 1 with their message.
if ($LASTEXITCODE -ne 0) {
Write-Error "OpenCode download failed (curl exit $LASTEXITCODE)." -ErrorAction Continue
exit $LASTEXITCODE
$binary = $null
# Unlike curl.exe, Invoke-WebRequest uses the system proxy and tolerates
# revocation servers a filtered network cannot reach.
try {
Invoke-WebRequest -UseBasicParsing -Uri $release -OutFile $archive -TimeoutSec 30
Expand-Archive -Path $archive -DestinationPath $temp -Force
$binary = Join-Path $temp 'opencode.exe'
} catch {
Write-Output "GitHub download failed: $($_.Exception.Message)"
}
# The same build is published to npm, which networks that block GitHub's downloads often allow.
if (-not $binary) {
try {
$package = Invoke-RestMethod -UseBasicParsing -Uri "https://registry.npmjs.org/opencode-windows-$asset/latest" -TimeoutSec 30
$tarball = Join-Path $temp 'opencode.tgz'
Invoke-WebRequest -UseBasicParsing -Uri $package.dist.tarball -OutFile $tarball -TimeoutSec 30
$expected = -join ([Convert]::FromBase64String(($package.dist.integrity -replace '^sha512-', '')) | ForEach-Object { $_.ToString('x2') })
if ((Get-FileHash -LiteralPath $tarball -Algorithm SHA512).Hash -ne $expected) { throw 'npm package checksum mismatch.' }
& (Join-Path $env:SystemRoot 'System32\tar.exe') -xzf $tarball -C $temp
if ($LASTEXITCODE -ne 0) { throw "tar exited with $LASTEXITCODE." }
$binary = Join-Path $temp 'package\bin\opencode.exe'
} catch {
Write-Output "npm download failed: $($_.Exception.Message)"
}
}
if (-not $binary) {
curl.exe -fL --retry 2 --connect-timeout 15 --max-time 300 $release -o $archive
# Keep curl's own exit code so distinct network faults stay apart; the
# throws below surface as exit 1 with their message.
if ($LASTEXITCODE -ne 0) {
# Straight to stderr: an error record raised here prints this whole script as its source.
[Console]::Error.WriteLine("OpenCode download failed (curl exit $LASTEXITCODE).")
exit $LASTEXITCODE
}
Expand-Archive -Path $archive -DestinationPath $temp -Force
$binary = Join-Path $temp 'opencode.exe'
}
Expand-Archive -Path $archive -DestinationPath $temp
$binary = Join-Path $temp 'opencode.exe'
& $binary --version
if ($LASTEXITCODE -ne 0) { throw 'OpenCode failed to run after extraction.' }
New-Item -ItemType Directory -Path $destination -Force | Out-Null
Expand Down
2 changes: 1 addition & 1 deletion src/local/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn usable_bash() -> Option<PathBuf> {
pub fn missing_toolchain() -> Option<&'static str> {
usable_bash().is_none().then_some(
"Git for Windows not found — orx needs the bash it ships to run experiments. \
Install it from https://git-scm.com/download/win, then restart orx.",
Install it from the dashboard or from https://git-scm.com/download/win.",
)
}

Expand Down
2 changes: 1 addition & 1 deletion src/local/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use tokio::process::Command;

use crate::error::{anyhow, Result};

const UA: &str = concat!("orx/", env!("CARGO_PKG_VERSION"));
pub(crate) const UA: &str = concat!("orx/", env!("CARGO_PKG_VERSION"));
pub const SHALLOW_CLONE_THRESHOLD_KB: u64 = 250 * 1024;

#[derive(Clone, Copy)]
Expand Down
1 change: 1 addition & 0 deletions src/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub mod opencode;
pub mod openresearch;
pub mod overleaf;
pub mod overleaf_live;
pub mod portable_git;
pub mod projects;
pub mod ray;
pub mod resolve;
Expand Down
Loading
Loading