From bfd50bad1811860ceb74ce40713f032e85040890 Mon Sep 17 00:00:00 2001 From: Ankur Goyal Date: Sun, 8 Feb 2026 15:31:57 -0800 Subject: [PATCH 1/2] make runtime selector better --- Cargo.lock | 2 +- README.md | 10 +++++++ src/eval.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f4fb8c..1c00b26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,7 +130,7 @@ dependencies = [ [[package]] name = "bt" -version = "0.1.1" +version = "0.1.2" dependencies = [ "anyhow", "braintrust-sdk-rust", diff --git a/README.md b/README.md index f52712a..42ed2b2 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,16 @@ Remove-Item -Recurse -Force (Join-Path $env:APPDATA "bt") -ErrorAction SilentlyC - If `bt self update --check` hits GitHub API limits in CI, set `GITHUB_TOKEN` in the environment. - If your network blocks GitHub asset downloads, install from a machine with direct access or configure your proxy/firewall to allow `github.com` and `api.github.com`. +## `bt eval` runners + +- By default, `bt eval` auto-detects a JavaScript runner from your project (`tsx`, `vite-node`, `ts-node`, then `ts-node-esm`). +- You can also set a runner explicitly with `--runner`: + - `bt eval --runner vite-node tutorial.eval.ts` + - `bt eval --runner tsx tutorial.eval.ts` +- You do not need to pass a full path for common runners; `bt` resolves local `node_modules/.bin` entries automatically. +- If eval execution fails with ESM/top-level-await related errors, retry with: + - `bt eval --runner vite-node tutorial.eval.ts` + ## Roadmap / TODO - Add richer channel controls for self-update (for example pinned/branch canary selection). diff --git a/src/eval.rs b/src/eval.rs index 1228d61..774dda7 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -109,6 +109,8 @@ async fn run_eval_files( no_send_logs: bool, ) -> Result<()> { let language = detect_eval_language(&files, language_override)?; + let show_js_runner_hint_on_failure = + language == EvalLanguage::JavaScript && runner_override.is_none(); let (js_runner, py_runner) = prepare_eval_runners()?; let socket_path = build_sse_socket_path()?; @@ -220,6 +222,11 @@ async fn run_eval_files( if let Some(status) = status { if !status.success() { + if show_js_runner_hint_on_failure { + anyhow::bail!( + "eval runner exited with status {status}\nHint: If this eval uses ESM features (like top-level await), try `--runner vite-node`." + ); + } anyhow::bail!("eval runner exited with status {status}"); } } @@ -285,8 +292,9 @@ fn build_js_command( files: &[String], ) -> Result { let command = if let Some(explicit) = runner_override.as_deref() { - let runner_script = select_js_runner_entrypoint(runner, Path::new(explicit))?; - let mut command = Command::new(explicit); + let resolved_runner = resolve_js_runner_command(explicit, files); + let runner_script = select_js_runner_entrypoint(runner, resolved_runner.as_ref())?; + let mut command = Command::new(resolved_runner); command.arg(runner_script).args(files); command } else if let Some(auto_runner) = find_js_runner_binary(files) { @@ -334,6 +342,41 @@ fn find_js_runner_binary(files: &[String]) -> Option { // preferred, with ts-node variants as lower-priority fallback. const RUNNER_CANDIDATES: &[&str] = &["tsx", "vite-node", "ts-node", "ts-node-esm"]; + for candidate in RUNNER_CANDIDATES { + if let Some(path) = find_node_module_bin_for_files(candidate, files) { + return Some(path); + } + } + + find_binary_in_path(RUNNER_CANDIDATES) +} + +fn resolve_js_runner_command(runner: &str, files: &[String]) -> PathBuf { + if is_path_like_runner(runner) { + return PathBuf::from(runner); + } + + find_node_module_bin_for_files(runner, files) + .or_else(|| find_binary_in_path(&[runner])) + .unwrap_or_else(|| PathBuf::from(runner)) +} + +fn is_path_like_runner(runner: &str) -> bool { + let path = Path::new(runner); + path.is_absolute() || runner.contains('/') || runner.contains('\\') || runner.starts_with('.') +} + +fn find_node_module_bin_for_files(binary: &str, files: &[String]) -> Option { + let search_roots = js_runner_search_roots(files); + for root in &search_roots { + if let Some(path) = find_node_module_bin(binary, root) { + return Some(path); + } + } + None +} + +fn js_runner_search_roots(files: &[String]) -> Vec { let mut search_roots = Vec::new(); if let Ok(cwd) = std::env::current_dir() { search_roots.push(cwd.clone()); @@ -349,16 +392,7 @@ fn find_js_runner_binary(files: &[String]) -> Option { } } } - - for candidate in RUNNER_CANDIDATES { - for root in &search_roots { - if let Some(path) = find_node_module_bin(candidate, root) { - return Some(path); - } - } - } - - find_binary_in_path(RUNNER_CANDIDATES) + search_roots } fn select_js_runner_entrypoint(default_runner: &Path, runner_command: &Path) -> Result { @@ -1237,6 +1271,25 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + #[test] + fn resolve_js_runner_command_finds_local_node_module_bin() { + let dir = unique_test_dir("resolve-runner"); + let eval_dir = dir.join("evals"); + let bin_dir = dir.join("node_modules").join(".bin"); + std::fs::create_dir_all(&eval_dir).expect("eval dir should be created"); + std::fs::create_dir_all(&bin_dir).expect("bin dir should be created"); + let local_runner = bin_dir.join("vite-node"); + std::fs::write(&local_runner, "echo").expect("local runner should be written"); + + let file = eval_dir.join("sample.eval.ts"); + let files = vec![file.to_string_lossy().to_string()]; + + let resolved = resolve_js_runner_command("vite-node", &files); + assert_eq!(resolved, local_runner); + + let _ = std::fs::remove_dir_all(&dir); + } + #[test] fn box_with_title_handles_ansi_content_without_panicking() { let content = "plain line\n\x1b[38;5;196mred text\x1b[0m"; From ddd345b814daa405be14c7516f9c10e788550e54 Mon Sep 17 00:00:00 2001 From: Ankur Goyal Date: Sun, 8 Feb 2026 15:55:18 -0800 Subject: [PATCH 2/2] print pr release --- .github/workflows/release-canary.yml | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml index 09e238c..f766715 100644 --- a/.github/workflows/release-canary.yml +++ b/.github/workflows/release-canary.yml @@ -12,6 +12,8 @@ concurrency: permissions: contents: write + issues: write + pull-requests: write env: CARGO_NET_GIT_FETCH_WITH_CLI: true @@ -329,6 +331,51 @@ jobs: --notes-file "$RUNNER_TEMP/canary-latest-notes.md" \ artifacts/* + comment-pr-install: + needs: + - plan + - announce + if: ${{ github.event_name == 'push' && needs.announce.result == 'success' }} + runs-on: ubuntu-22.04 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + OWNER: ${{ github.repository_owner }} + BRANCH_NAME: ${{ github.ref_name }} + SHORT_SHA: ${{ needs.plan.outputs.short-sha }} + ALIAS_TAG: ${{ needs.plan.outputs.alias-tag }} + steps: + - name: Upsert PR install comment + shell: bash + run: | + pr_number="$(gh api "repos/${REPO}/pulls" -f state=open -f head="${OWNER}:${BRANCH_NAME}" --jq '.[0].number // empty')" + if [ -z "$pr_number" ]; then + echo "No open PR found for ${OWNER}:${BRANCH_NAME}; skipping PR install comment." + exit 0 + fi + + marker="" + body_file="$RUNNER_TEMP/pr-canary-install-comment.md" + cat > "$body_file" < "$payload_file" + + comment_id="$(gh api "repos/${REPO}/issues/${pr_number}/comments" --paginate --jq ".[] | select(.body | contains(\"${marker}\")) | .id" | head -n1)" + if [ -n "$comment_id" ]; then + gh api --method PATCH "repos/${REPO}/issues/comments/${comment_id}" --input "$payload_file" >/dev/null + echo "Updated PR #${pr_number} install comment (${comment_id})." + else + gh api --method POST "repos/${REPO}/issues/${pr_number}/comments" --input "$payload_file" >/dev/null + echo "Created PR #${pr_number} install comment." + fi + smoke-install-unix: needs: - plan