Skip to content

feat: default fast execution for run and exec#2

Merged
spa5k merged 4 commits intomainfrom
feat/native-execution-defaults
Mar 26, 2026
Merged

feat: default fast execution for run and exec#2
spa5k merged 4 commits intomainfrom
feat/native-execution-defaults

Conversation

@spa5k
Copy link
Copy Markdown
Collaborator

@spa5k spa5k commented Mar 24, 2026

Summary

  • make the fast/native execution ladder the default for nr and nlx
  • prefer builtin node --run for node run when the installed Node supports it and the script is safe for that path
  • add project-state caching, execution-mode reporting, profiling hooks, and remove benchmark CI runs

Details

  • keep mutation/package-manager commands delegated while focusing native execution on run/exec flows
  • keep nr on native -> node --run -> package manager
  • route node run through node --run -> native -> package manager
  • add cached real-node --run capability detection and surface it through doctor/explain
  • replace one-shot profile collection with the internal loop profiler
  • remove the benchmark GitHub Actions workflow

Testing

  • cargo test -- --test-threads=1
  • release sanity benchmarks for nr and node run fast paths versus --no-native

Summary by CodeRabbit

  • New Features

    • Added --fast as an alias for native execution
    • Built-in Node --run support with safe fallbacks
    • Internal profile-loop command for repeatable profiling
  • Improvements

    • Native execution enabled by default for common commands
    • Updated CLI help/examples and clearer execution status in doctor output
    • Diagnose/report now shows node_run_supported
  • Chores

    • Removed repository benchmark workflow and a README sentence about it

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e26f88a0-6516-4e43-aa72-999de66144df

📥 Commits

Reviewing files that changed from the base of the PR and between 97a51c3 and 6bbff71.

📒 Files selected for processing (1)
  • tests/parity_against_antfu.rs

📝 Walkthrough

Walkthrough

Removed the Benchmark GitHub Actions workflow and one README sentence; made native-mode the default and added --fast as an alias for --native; introduced a hidden internal profile-loop command; added ExecutionMode tracking and guarded Node --run fallbacks; refactored ResolveContext to cache ProjectState for detection.

Changes

Cohort / File(s) Summary
CI / Docs
\.github/workflows/benchmark.yml, README.md
Deleted the Benchmark workflow and removed the README line about auto-updating benchmark PR comments.
Benchmark scripts
benchmark/profile.sh
Switched profiling invocations to call internal profile-loop --iterations "$ITERATIONS" and added HNI_PROFILE_ITERATIONS (default 4000).
CLI parsing & help
src/app/cli.rs, src/app/help.rs
Added hidden internal profile-loop subcommand; added --fast as alias for --native (conflicts with --no-native); updated help text and examples.
Dispatch & internal command
src/app/dispatch.rs
Added InternalProfileLoop handling that repeatedly dispatches/ formats invocations; changed ResolveContext construction to use package-manager availability checks; updated explain printing.
Doctor output
src/app/doctor.rs
print_doctor now accepts &ResolveContext, uses ctx.detect(), prints node_run_supported:, and updates node label text.
Config & defaults
src/core/config.rs, tests/config_detect.rs
HniConfig::default() now sets native_mode = true; tests updated to expect native_mode true.
Execution modes & types
src/core/types.rs, src/core/batch.rs
Added ExecutionMode enum and mode field on ResolvedExecution; added constructors and execution_mode_name(); internal executions mark Internal.
ResolveContext & ProjectState
src/core/resolve/context.rs
Added with_package_manager_checks, cached ProjectState (OnceLock), project_state() accessor, detect() and cwd(); tracks verify_package_manager_availability.
Detection & availability checks
src/core/resolve/detect.rs, src/core/detect.rs
Switched detection APIs to accept ResolveContext; added version_hint to AgentResolution; moved availability gating to ensure_detected_available; exposed two helpers as pub(crate).
Resolve build & node-run routing
src/core/resolve/build.rs, src/core/resolve/mod.rs
Added resolve_node_run, routed Intent::Run to node-run resolver, added build_node_run_exec_if_safe and legacy fallback, and ensured detected availability before building execs; exported resolve_node_run.
Native resolution changes
src/core/native.rs
attempt_nr/attempt_nlx now take Option<PackageManager>; use ProjectState for nearest package, bin dirs, and Yarn PnP detection; adjusted resolution order and returned paths.
Platform node capability
src/platform/node.rs
Added pub fn real_node_supports_run() with OnceLock caching and 1s probe of node --help; added tests for detection, env override, and timeout.
Feature call sites
src/features/..., src/app/version.rs
Call sites updated to use ctx.cwd() accessor (pass-by-value) instead of borrowing &ctx.cwd.
Tests & contracts
tests/...
tests/cli_contract.rs, tests/native_execution.rs, tests/resolve_matrix.rs, tests/init_contract.rs, tests/parity_against_antfu.rs
Added/updated tests for default fast/native behavior, --fast alias, internal profile-loop, node-run detection/fallbacks, doctor output, PATH override helpers, and test environment adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI
    participant Resolve as Resolver
    participant Project as ProjectState
    participant NodeProbe as NodeProbe
    participant PM as PackageManager

    CLI->>Resolve: resolve_node_run(args, ctx)
    Resolve->>Project: ctx.project_state() (lazy scan)
    Resolve->>Resolve: attempt_nr(pm_hint, args, ctx)
    alt native_mode & native eligible
        Resolve-->>CLI: Resolved Native execution (mode: native)
    else native_mode & native ineligible
        Resolve->>NodeProbe: build_node_run_exec_if_safe(ctx, script)
        alt node --run supported & safe
            NodeProbe-->>Resolve: Node --run execution (mode: node-run)
            Resolve-->>CLI: Resolved NodeRun execution
        else unsafe/unsupported
            Resolve->>PM: build_exec(Intent::Run) (mode: package-manager)
            PM-->>Resolve: Package-manager execution
            Resolve-->>CLI: Resolved PackageManager execution
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐇 I tunneled through code with a twitch and a hop,
--fast now sprints while old checks stop.
Node --run peeks, then yields to the safe,
ProjectState caches keep pace with our race.
Tiny paws profile loops — hop, hop, hop!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: default fast execution for run and exec' accurately summarizes the main change: making fast/native execution the default behavior for run and exec commands.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/native-execution-defaults

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/cli.rs (1)

549-558: ⚠️ Potential issue | 🟡 Minor

Mention --fast in the conflict error.

--fast --no-native currently reports --native conflicts with --no-native, which points at a flag the user never passed.

✏️ Suggested tweak
-            Err(HniError::parse("--native conflicts with --no-native"))
+            Err(HniError::parse("--native/--fast conflicts with --no-native"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/cli.rs` around lines 549 - 558, The conflict error message in
set_native_override reports "--native conflicts with --no-native" which is
misleading when the true source is the --fast flag; update the error text in
set_native_override (the HniError::parse call) to mention --fast as well (for
example "--native/--fast conflicts with --no-native" or otherwise include
"--fast") so users who passed --fast see the correct conflict candidate; change
only the string passed to HniError::parse in the match arm that handles
Some(existing) if existing != value.
🧹 Nitpick comments (2)
tests/resolve_matrix.rs (1)

327-330: Use a drop guard for HNI_REAL_NODE cleanup.

These cases restore the process env manually after the unwrap()/asserts. If one assertion fails early, later tests in the same process can inherit the fake node path and fail for the wrong reason. A small Drop-based helper would make this suite non-leaky.

Also applies to: 355-358, 384-387

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/resolve_matrix.rs` around lines 327 - 330, The tests set HNI_REAL_NODE
using support::set_var and then call resolve::resolve_node_run, but they
manually call support::remove_var afterwards which leaks if an assertion panics;
create a small Drop-based helper (e.g., EnvVarGuard or HniRealNodeGuard) that
captures the previous value (or absence) of the HNI_REAL_NODE env var in its
constructor when you call support::set_var and restores or removes it in Drop,
then replace the set_var/remove_var pairs in the failing test blocks (and the
similar locations around resolve_node_run calls at the other lines mentioned)
with creation of this guard so the environment is always restored even if tests
panic or unwind.
src/core/resolve/detect.rs (1)

24-69: Make the validated path harder to skip.

detect_for_action() now returns a package manager even when the binary is unavailable, and every external-exec caller has to remember to add ensure_detected_available() later. I'd consider renaming the raw helper or returning a validated wrapper so future resolver paths don't regress to late command not found failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/resolve/detect.rs` around lines 24 - 69, detect_for_action currently
returns an AgentResolution even if the agent binary is missing, relying on
callers to call ensure_detected_available; change this so callers can't easily
forget the validation: rename the existing detect_for_action to
detect_raw_for_action (or similar) to preserve behavior, then implement a new
detect_for_action that calls the raw detector and immediately validates via
ensure_package_manager_available (using resolution.pm and
resolution.version_hint.as_deref()) and maps validation errors into
HniError::detection before returning the validated AgentResolution; update call
sites to use the new detect_for_action or the raw name where unvalidated
behavior is intentional.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/native.rs`:
- Around line 157-158: The chained bin resolution expression assigned to
bin_path is misformatted for rustfmt; update the wrapping so rustfmt accepts it
by placing the first call on its own line and the or_else chain indented on the
next line (e.g., start with let bin_path = resolve_local_bin(bin_name,
&bin_paths) and then call .or_else(|) on the next line pointing to
state.resolve_declared_package_bin(bin_name)); ensure you reference
resolve_local_bin, resolve_declared_package_bin and the bin_path assignment and
keep commas/parentheses unchanged so the expression remains a single let
binding.

In `@src/core/resolve/build.rs`:
- Line 97: The eager call to detect_for_action(ctx, false) aborts fast-path
runs; change the flow so build_node_run_exec_if_safe(...) is attempted first and
only call detect_for_action(...) when the safe node-run path returns None and
you need the package-manager fallback. Concretely, remove or defer the immediate
assignment to detected from detect_for_action at the earlier sites and instead:
call build_node_run_exec_if_safe(ctx, ...) first, if it returns Some(exec) use
that, otherwise call detect_for_action(ctx, false) and proceed with the existing
package-manager fallback logic using the detected value; apply the same change
at both occurrences where detect_for_action is currently invoked.
- Around line 117-126: The code returns build_legacy_node_run_exec whenever
ctx.config.run_agent == RunAgent::Node even if build_node_run_exec_if_safe
returned None (meaning unsupported/unsafe/missing script), which bypasses the
new safety gate; change the logic so that build_legacy_node_run_exec is only
returned when build_node_run_exec_if_safe actually produced a Some(resolved)
(i.e., keep the current return inside the Some branch) and remove/avoid the
unconditional fallback to build_legacy_node_run_exec when
build_node_run_exec_if_safe returned None; update both call sites that use
build_node_run_exec_if_safe and the conditional on ctx.config.run_agent ==
RunAgent::Node (the occurrences around build_node_run_exec_if_safe and
build_legacy_node_run_exec) so a None result falls back to the package manager
path instead of calling build_legacy_node_run_exec.

In `@src/core/resolve/context.rs`:
- Around line 15-45: ResolveContext currently exposes a mutable cwd while
caching ProjectState in project_state, causing stale state if cwd is changed
after the first project_state() or detect() call; fix by making cwd
private/immutable (remove pub on cwd and provide read-only accessor) so callers
cannot mutate it after construction, or—if public mutation is required—add cache
invalidation logic: whenever cwd is changed (e.g., provide a set_cwd method or
wrap existing mutation points), call project_state.take() or recreate/reset
project_state so ProjectState::scan will run against the new cwd; update uses of
ResolveContext::new, ResolveContext::project_state, and ResolveContext::detect
accordingly.

In `@src/core/types.rs`:
- Around line 43-49: The ExecutionMode enum and its external() constructor are
mislabeling non-package-manager executions; add a neutral variant (e.g.,
External or Passthrough) to ExecutionMode (in the enum where
PackageManager/NodeRun/Native/Internal are defined) and change external() to
return that neutral variant instead of PackageManager; then update callers that
need specific semantics (notably resolve_node_passthrough() in resolve/build.rs)
to either construct the correct explicit mode (e.g., ExecutionMode::NodeRun) or
continue using the new neutral ExecutionMode::External/Passthrough where
appropriate; also apply the same change to the other external() usage locations
referenced around lines 88-95 so nothing is implicitly labeled PackageManager.

In `@src/platform/node.rs`:
- Around line 75-82: The current probe_node_run_support function runs `node
--help` without a timeout and can hang; update probe_node_run_support to spawn
the child (using Command::new(node_path).arg("--help").spawn()), wait with a
short Duration (e.g. a few hundred ms) and, on timeout, kill/terminate the child
and return false (treat timeout as unsupported). After successful completion,
capture stdout/stderr and pass them to help_text_supports_run as before; ensure
any spawn/wait/kill errors are handled by returning false. Use the existing
function name probe_node_run_support and helper help_text_supports_run when
locating where to implement the change.

---

Outside diff comments:
In `@src/app/cli.rs`:
- Around line 549-558: The conflict error message in set_native_override reports
"--native conflicts with --no-native" which is misleading when the true source
is the --fast flag; update the error text in set_native_override (the
HniError::parse call) to mention --fast as well (for example "--native/--fast
conflicts with --no-native" or otherwise include "--fast") so users who passed
--fast see the correct conflict candidate; change only the string passed to
HniError::parse in the match arm that handles Some(existing) if existing !=
value.

---

Nitpick comments:
In `@src/core/resolve/detect.rs`:
- Around line 24-69: detect_for_action currently returns an AgentResolution even
if the agent binary is missing, relying on callers to call
ensure_detected_available; change this so callers can't easily forget the
validation: rename the existing detect_for_action to detect_raw_for_action (or
similar) to preserve behavior, then implement a new detect_for_action that calls
the raw detector and immediately validates via ensure_package_manager_available
(using resolution.pm and resolution.version_hint.as_deref()) and maps validation
errors into HniError::detection before returning the validated AgentResolution;
update call sites to use the new detect_for_action or the raw name where
unvalidated behavior is intentional.

In `@tests/resolve_matrix.rs`:
- Around line 327-330: The tests set HNI_REAL_NODE using support::set_var and
then call resolve::resolve_node_run, but they manually call support::remove_var
afterwards which leaks if an assertion panics; create a small Drop-based helper
(e.g., EnvVarGuard or HniRealNodeGuard) that captures the previous value (or
absence) of the HNI_REAL_NODE env var in its constructor when you call
support::set_var and restores or removes it in Drop, then replace the
set_var/remove_var pairs in the failing test blocks (and the similar locations
around resolve_node_run calls at the other lines mentioned) with creation of
this guard so the environment is always restored even if tests panic or unwind.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b17ac87d-0912-46c4-a075-ce5b930d8f50

📥 Commits

Reviewing files that changed from the base of the PR and between dae6f5e and 19f01ea.

📒 Files selected for processing (21)
  • .github/workflows/benchmark.yml
  • README.md
  • benchmark/profile.sh
  • src/app/cli.rs
  • src/app/dispatch.rs
  • src/app/doctor.rs
  • src/app/help.rs
  • src/core/config.rs
  • src/core/detect.rs
  • src/core/native.rs
  • src/core/resolve/build.rs
  • src/core/resolve/context.rs
  • src/core/resolve/detect.rs
  • src/core/resolve/mod.rs
  • src/core/types.rs
  • src/platform/node.rs
  • tests/cli_contract.rs
  • tests/config_detect.rs
  • tests/init_contract.rs
  • tests/native_execution.rs
  • tests/resolve_matrix.rs
💤 Files with no reviewable changes (2)
  • README.md
  • .github/workflows/benchmark.yml

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
src/core/resolve/build.rs (2)

107-108: ⚠️ Potential issue | 🟠 Major

Defer agent detection until the run fallback is actually needed.

Line 108 and Line 187 still force detect_for_action() before the native / node --run branches run. That means nr / node run can still fail in a repo with a runnable package.json script but no detectable package manager, even though the fast path itself does not need the delegated fallback yet. Move the hard detection step after the fast-path attempts.

Also applies to: 187-187

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/resolve/build.rs` around lines 107 - 108, The resolve_nr function is
calling detect_for_action() too early and forces agent detection before the
fast-path native / `node --run` branches execute; update resolve_nr to postpone
invoking detect_for_action() (and any use of its result) until after you attempt
the fast-path resolution (the native branch and node --run branch) so that a
runnable package.json script succeeds without a detectable package manager, and
similarly move the deferred detection in the fallback logic that currently
references detect_for_action() (also mentioned at the later fallback location)
so detection only occurs when the delegated fallback is actually needed.

136-137: ⚠️ Potential issue | 🟠 Major

Don't route back to raw node --run after the safety check already rejected it.

Once build_node_run_exec_if_safe() returns None, Lines 136-137 and 161-163 still emit build_legacy_node_run_exec(). That bypasses the guard for missing scripts, unsupported env expansion, pre/post hooks, or unsupported Node builds and lands on the exact node --run path that was just ruled out. This branch should fall back to the package-manager path instead.

Also applies to: 161-163

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/resolve/build.rs` around lines 136 - 137, The code currently returns
build_legacy_node_run_exec whenever ctx.config.run_agent == RunAgent::Node even
if build_node_run_exec_if_safe() previously returned None; change both places
(the branch at the first occurrence and the later one) to first call
build_node_run_exec_if_safe(args, ctx) and, if it returns Some(exec), return
that, but if it returns None do NOT call build_legacy_node_run_exec — instead
fall back to the package-manager execution path used elsewhere in this function
(i.e., follow the same fallback logic as the non-Node branches). Ensure you
replace the unconditional build_legacy_node_run_exec calls with this guarded
behavior around build_node_run_exec_if_safe().
src/core/resolve/context.rs (1)

16-20: ⚠️ Potential issue | 🟠 Major

Cached project state can go stale after cwd mutation.

Line 17 keeps cwd public while Lines 19-20 cache the first ProjectState. Any later ctx.cwd update leaves detection, .bin lookup, and Yarn PnP checks pinned to the old tree. Please make cwd immutable/private or invalidate project_state on every mutation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/resolve/context.rs` around lines 16 - 20, ResolveContext currently
exposes pub cwd while caching a single ProjectState in project_state (OnceLock),
which lets external mutations to cwd leave project_state stale; fix by making
cwd private (change pub cwd: PathBuf to cwd: PathBuf) and expose a read-only
accessor like fn cwd(&self) -> &Path to prevent external mutation, or if you
must allow cwd changes replace project_state/OnceLock with a resettable
container and add a set_cwd(&mut self, new: PathBuf) that updates cwd and
clears/invalidates project_state before returning (referencing ResolveContext,
cwd, project_state, ProjectState, OnceLock).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/cli.rs`:
- Around line 357-361: The invocation argument parser currently omits "np" and
"ns", so update the PossibleValuesParser list passed to Arg::new("invocation")
to include "np" and "ns" and also ensure internal_invocation() recognizes these
strings and maps them to InvocationKind::Np and InvocationKind::Ns (or better:
factor the mapping into a shared lookup used by both the Arg parser and
internal_invocation()/dispatch_invocation() to avoid drift); touch the other
parser block around the 415-424 region the comment mentions so both places use
the same canonical set or mapping for invocation names.

In `@src/app/dispatch.rs`:
- Around line 55-61: Change the matches! that computes
verify_package_manager_availability to borrow parsed.command instead of moving
it: use matches!(&parsed.command, ParsedCommand::Execute { .. }) so
parsed.command remains usable later; keep the same logic and pass the resulting
boolean into ResolveContext::with_package_manager_checks (which takes
parsed.cwd.clone(), config.clone(), verify_package_manager_availability).

In `@src/core/resolve/build.rs`:
- Around line 253-256: In resolve_nlx, don't call detect_for_action() before
attempting the native local-bin fast path; instead, if ctx.config.native_mode is
true, first call native::attempt_nlx(detected_pm?, &args, ctx) but without
earlier detection—invoke native::attempt_nlx directly and only call
detect_for_action() after that attempt fails so you can supply the detected
package manager for the delegated fallback; update resolve_nlx to defer
detect_for_action() and only use its result for the fallback branch.

---

Duplicate comments:
In `@src/core/resolve/build.rs`:
- Around line 107-108: The resolve_nr function is calling detect_for_action()
too early and forces agent detection before the fast-path native / `node --run`
branches execute; update resolve_nr to postpone invoking detect_for_action()
(and any use of its result) until after you attempt the fast-path resolution
(the native branch and node --run branch) so that a runnable package.json script
succeeds without a detectable package manager, and similarly move the deferred
detection in the fallback logic that currently references detect_for_action()
(also mentioned at the later fallback location) so detection only occurs when
the delegated fallback is actually needed.
- Around line 136-137: The code currently returns build_legacy_node_run_exec
whenever ctx.config.run_agent == RunAgent::Node even if
build_node_run_exec_if_safe() previously returned None; change both places (the
branch at the first occurrence and the later one) to first call
build_node_run_exec_if_safe(args, ctx) and, if it returns Some(exec), return
that, but if it returns None do NOT call build_legacy_node_run_exec — instead
fall back to the package-manager execution path used elsewhere in this function
(i.e., follow the same fallback logic as the non-Node branches). Ensure you
replace the unconditional build_legacy_node_run_exec calls with this guarded
behavior around build_node_run_exec_if_safe().

In `@src/core/resolve/context.rs`:
- Around line 16-20: ResolveContext currently exposes pub cwd while caching a
single ProjectState in project_state (OnceLock), which lets external mutations
to cwd leave project_state stale; fix by making cwd private (change pub cwd:
PathBuf to cwd: PathBuf) and expose a read-only accessor like fn cwd(&self) ->
&Path to prevent external mutation, or if you must allow cwd changes replace
project_state/OnceLock with a resettable container and add a set_cwd(&mut self,
new: PathBuf) that updates cwd and clears/invalidates project_state before
returning (referencing ResolveContext, cwd, project_state, ProjectState,
OnceLock).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18db1b36-1852-47d6-9b42-02135416f7d4

📥 Commits

Reviewing files that changed from the base of the PR and between 19f01ea and 01fd37d.

📒 Files selected for processing (11)
  • src/app/cli.rs
  • src/app/dispatch.rs
  • src/core/native.rs
  • src/core/resolve/build.rs
  • src/core/resolve/context.rs
  • src/core/resolve/detect.rs
  • src/core/types.rs
  • src/platform/node.rs
  • tests/cli_contract.rs
  • tests/native_execution.rs
  • tests/resolve_matrix.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/core/types.rs
  • src/platform/node.rs
  • tests/cli_contract.rs

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/platform/node.rs (1)

315-374: Use one shared env-test guard for these cases.

These tests only serialize against each other. They do not coordinate with the separate environment lock already used by the integration suite, so HNI_REAL_NODE can still race under default parallel test execution. Please move them behind the same process-wide test helper (or into the integration suite) instead of keeping a second lock here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/node.rs` around lines 315 - 374, These two tests
(real_node_supports_run_uses_env_override and
real_node_supports_run_times_out_for_hanging_help) currently use the local
ENV_LOCK guard and therefore only serialize against each other; replace that
local lock usage with the project’s single process-wide env-test helper so they
serialize with the integration suite as well. Specifically, remove the
ENV_LOCK.get_or_init()...lock() block at the start of each test and instead wrap
the test body that touches REAL_NODE_ENV in the shared helper (the existing
process-wide env-test helper used elsewhere in the repo), preserving the same
logic that sets/restores REAL_NODE_ENV and filesystem setup so behavior of
real_node_supports_run remains unchanged.
tests/resolve_matrix.rs (1)

389-392: Consider RAII guard for HNI_REAL_NODE cleanup.

The current pattern sets HNI_REAL_NODE at line 389 and removes it at line 392, but if the test panics between these lines, the env var won't be cleaned up. This could affect other tests. Consider using an RAII guard similar to with_path_override for consistency and safety.

♻️ Suggested approach
fn with_real_node_override<T>(path: &Path, f: impl FnOnce() -> T) -> T {
    struct RealNodeGuard(Option<std::ffi::OsString>);
    impl Drop for RealNodeGuard {
        fn drop(&mut self) {
            match &self.0 {
                Some(value) => support::set_var("HNI_REAL_NODE", value),
                None => support::remove_var("HNI_REAL_NODE"),
            }
        }
    }

    let original = std::env::var_os("HNI_REAL_NODE");
    support::set_var("HNI_REAL_NODE", path);
    let guard = RealNodeGuard(original);
    let out = f();
    drop(guard);
    out
}

Then usage becomes:

with_real_node_override(&fake_node, || {
    let ctx = ResolveContext::new(...);
    let resolved = resolve::resolve_node_run(...);
    // assertions
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/resolve_matrix.rs` around lines 389 - 392, Replace the manual
set/remove of the HNI_REAL_NODE env var with an RAII guard to ensure cleanup on
panic: implement a helper like with_real_node_override that captures the
original std::env::var_os("HNI_REAL_NODE"), calls
support::set_var("HNI_REAL_NODE", &fake_node), and returns a guard whose Drop
restores the original value or calls support::remove_var; then wrap the
ResolveContext::new(...) / resolve::resolve_node_run(...) invocation inside that
helper (similar to with_path_override) so the env var is always restored even if
the test panics.
src/app/dispatch.rs (1)

157-157: Clarify the detection fallback pattern.

The .or_else(|_| detect(ctx.cwd(), &ctx.config)) suggests ctx.detect() might fail in explain mode while standalone detect might succeed. However, both should have the same behavior since ctx.detect() calls detect() internally. This fallback appears to be defensive code that may never be exercised.

Consider either documenting why this fallback is needed or simplifying to just ctx.detect() if the fallback is not necessary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/dispatch.rs` at line 157, The fallback using ctx.detect().or_else(|_|
detect(ctx.cwd(), &ctx.config)) is redundant because ctx.detect() already
delegates to detect(), so either remove the fallback and call ctx.detect()
directly, or if you intend to keep defensive behavior, add a brief comment above
the call explaining why the standalone detect(ctx.cwd(), &ctx.config) is
necessary (for example: explain mode differences or historical edge cases).
Update the code around ctx.detect() in dispatch.rs accordingly and ensure tests
still pass.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/resolve/build.rs`:
- Around line 499-532: The calls to ctx.project_state() currently use ? which
propagates errors instead of falling back; fetch project_state once and convert
failures into Ok(None) (e.g., use a match or if let to return Ok(None) on Err),
then reuse that project_state for nearest_package() and has_yarn_pnp_loader()
checks; update build_node_run_exec_if_safe to obtain project_state with
error-to-Ok(None) handling and then proceed with let Some(pkg) =
project_state.nearest_package() else return Ok(None) and use
project_state.has_yarn_pnp_loader() where needed.

In `@src/platform/node.rs`:
- Around line 90-113: The current probe inspects stdout/stderr even when the
child process exits with a non-zero status, which can falsely detect support;
modify the logic around the child process handling in the node --help probe (the
code that uses child.try_wait(), child.wait(), and help_text_supports_run) to
check the exit status (Status from child.wait()) and if !status.success() return
false (treat as unsupported) before calling help_text_supports_run on
stdout/stderr; ensure you still drain stdout/stderr safely after waiting, but do
not consider their contents unless status.success() is true.

---

Nitpick comments:
In `@src/app/dispatch.rs`:
- Line 157: The fallback using ctx.detect().or_else(|_| detect(ctx.cwd(),
&ctx.config)) is redundant because ctx.detect() already delegates to detect(),
so either remove the fallback and call ctx.detect() directly, or if you intend
to keep defensive behavior, add a brief comment above the call explaining why
the standalone detect(ctx.cwd(), &ctx.config) is necessary (for example: explain
mode differences or historical edge cases). Update the code around ctx.detect()
in dispatch.rs accordingly and ensure tests still pass.

In `@src/platform/node.rs`:
- Around line 315-374: These two tests (real_node_supports_run_uses_env_override
and real_node_supports_run_times_out_for_hanging_help) currently use the local
ENV_LOCK guard and therefore only serialize against each other; replace that
local lock usage with the project’s single process-wide env-test helper so they
serialize with the integration suite as well. Specifically, remove the
ENV_LOCK.get_or_init()...lock() block at the start of each test and instead wrap
the test body that touches REAL_NODE_ENV in the shared helper (the existing
process-wide env-test helper used elsewhere in the repo), preserving the same
logic that sets/restores REAL_NODE_ENV and filesystem setup so behavior of
real_node_supports_run remains unchanged.

In `@tests/resolve_matrix.rs`:
- Around line 389-392: Replace the manual set/remove of the HNI_REAL_NODE env
var with an RAII guard to ensure cleanup on panic: implement a helper like
with_real_node_override that captures the original
std::env::var_os("HNI_REAL_NODE"), calls support::set_var("HNI_REAL_NODE",
&fake_node), and returns a guard whose Drop restores the original value or calls
support::remove_var; then wrap the ResolveContext::new(...) /
resolve::resolve_node_run(...) invocation inside that helper (similar to
with_path_override) so the env var is always restored even if the test panics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 64fc72e1-cea4-4cd1-b6c5-f4717ba2167d

📥 Commits

Reviewing files that changed from the base of the PR and between 01fd37d and 97a51c3.

📒 Files selected for processing (18)
  • src/app/cli.rs
  • src/app/dispatch.rs
  • src/app/doctor.rs
  • src/app/version.rs
  • src/core/batch.rs
  • src/core/native.rs
  • src/core/resolve/build.rs
  • src/core/resolve/context.rs
  • src/core/resolve/detect.rs
  • src/core/types.rs
  • src/features/node_shim.rs
  • src/features/np.rs
  • src/features/nr.rs
  • src/features/ns.rs
  • src/features/nun.rs
  • src/platform/node.rs
  • tests/cli_contract.rs
  • tests/resolve_matrix.rs
✅ Files skipped from review due to trivial changes (1)
  • src/core/resolve/context.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/cli_contract.rs

Comment on lines +499 to +532
fn build_node_run_exec_if_safe(
pm: Option<PackageManager>,
args: &[String],
ctx: &ResolveContext,
has_if_present: bool,
) -> HniResult<Option<ResolvedExecution>> {
if pm == Some(PackageManager::Deno) {
return Ok(None);
}

if !real_node_supports_run() {
return Ok(None);
}

if has_if_present {
return Ok(None);
}

let Some(pkg) = ctx.project_state()?.nearest_package() else {
return Ok(None);
};
let scripts = pkg.manifest.scripts.unwrap_or_default();
let script_name = args.first().cloned().unwrap_or_else(|| "start".to_string());
let Some(script) = scripts.get(&script_name) else {
return Ok(None);
};

if scripts.contains_key(&format!("pre{script_name}"))
|| scripts.contains_key(&format!("post{script_name}"))
|| script_uses_node_run_unsupported_env(script)
|| ctx.project_state()?.has_yarn_pnp_loader()
{
return Ok(None);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are other places in the codebase that handle project_state errors gracefully
rg -n "project_state\(\)" --type rust -A 2 | head -50

Repository: happytoolin/hni

Length of output: 885


🏁 Script executed:

#!/bin/bash
# Find all callers of build_node_run_exec_if_safe
rg -n "build_node_run_exec_if_safe" --type rust

Repository: happytoolin/hni

Length of output: 348


🏁 Script executed:

#!/bin/bash
# Check the return type and error handling of resolve_nr and resolve_node_run
rg -n "fn resolve_nr|fn resolve_node_run" --type rust -A 1

Repository: happytoolin/hni

Length of output: 355


🏁 Script executed:

#!/bin/bash
# Look for how project_state errors are typically handled - check for .ok() or similar patterns
rg -n "project_state\(\)" --type rust -B 1 -A 3 | grep -A 3 "\.ok()" | head -30

Repository: happytoolin/hni

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Read lines 120-135 to see the full context of the first call at line 127
sed -n '120,135p' src/core/resolve/build.rs | cat -n

Repository: happytoolin/hni

Length of output: 853


🏁 Script executed:

#!/bin/bash
# Read lines 200-215 to see the full context of the second call at line 208
sed -n '200,215p' src/core/resolve/build.rs | cat -n

Repository: happytoolin/hni

Length of output: 784


🏁 Script executed:

#!/bin/bash
# Find callers of resolve_nr and resolve_node_run to see how they handle HniResult errors
rg -n "resolve_nr\(|resolve_node_run\(" --type rust -B 2 -A 2 | head -60

Repository: happytoolin/hni

Length of output: 3857


🏁 Script executed:

#!/bin/bash
# Check if there's error handling middleware or if errors are just bubbled up
rg -n "\.map_err|\.unwrap|match.*resolve_nr|match.*resolve_node_run" --type rust | head -20

Repository: happytoolin/hni

Length of output: 1622


Error propagation in project_state() calls prevents graceful fallback.

The ? operators at lines 517 and 529 propagate errors from ctx.project_state() instead of returning Ok(None). If package.json is malformed, the entire resolve_nr() or resolve_node_run() call will error rather than falling back to package manager execution. This is inconsistent with the function's design, which handles multiple unsafe conditions by returning Ok(None). Consider converting project_state() errors to Ok(None) for a truly graceful fallback strategy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/resolve/build.rs` around lines 499 - 532, The calls to
ctx.project_state() currently use ? which propagates errors instead of falling
back; fetch project_state once and convert failures into Ok(None) (e.g., use a
match or if let to return Ok(None) on Err), then reuse that project_state for
nearest_package() and has_yarn_pnp_loader() checks; update
build_node_run_exec_if_safe to obtain project_state with error-to-Ok(None)
handling and then proceed with let Some(pkg) = project_state.nearest_package()
else return Ok(None) and use project_state.has_yarn_pnp_loader() where needed.

Comment on lines +90 to +113
let deadline = Instant::now() + NODE_RUN_PROBE_TIMEOUT;
loop {
match child.try_wait() {
Ok(Some(_)) => break,
Ok(None) if Instant::now() < deadline => thread::sleep(Duration::from_millis(10)),
Ok(None) | Err(_) => {
let _ = child.kill();
let _ = child.wait();
return false;
}
}
}

let mut stdout = Vec::new();
let mut stderr = Vec::new();
if let Some(mut pipe) = child.stdout.take() {
let _ = pipe.read_to_end(&mut stdout);
}
if let Some(mut pipe) = child.stderr.take() {
let _ = pipe.read_to_end(&mut stderr);
}
let _ = child.wait();

help_text_supports_run(&stdout) || help_text_supports_run(&stderr)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Treat failed --help probes as unsupported.

This probe currently trusts the output even when node --help exits non-zero. A wrapper or bad HNI_REAL_NODE override that prints --run in its error text will return true here and can steer normal run flows onto an unsupported node --run path. Check status.success() before inspecting stdout/stderr.

Suggested fix
-    let deadline = Instant::now() + NODE_RUN_PROBE_TIMEOUT;
-    loop {
+    let deadline = Instant::now() + NODE_RUN_PROBE_TIMEOUT;
+    let status = loop {
         match child.try_wait() {
-            Ok(Some(_)) => break,
+            Ok(Some(status)) => break status,
             Ok(None) if Instant::now() < deadline => thread::sleep(Duration::from_millis(10)),
             Ok(None) | Err(_) => {
                 let _ = child.kill();
                 let _ = child.wait();
                 return false;
             }
         }
     }
+
+    if !status.success() {
+        return false;
+    }
 
     let mut stdout = Vec::new();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/node.rs` around lines 90 - 113, The current probe inspects
stdout/stderr even when the child process exits with a non-zero status, which
can falsely detect support; modify the logic around the child process handling
in the node --help probe (the code that uses child.try_wait(), child.wait(), and
help_text_supports_run) to check the exit status (Status from child.wait()) and
if !status.success() return false (treat as unsupported) before calling
help_text_supports_run on stdout/stderr; ensure you still drain stdout/stderr
safely after waiting, but do not consider their contents unless status.success()
is true.

@spa5k spa5k merged commit 013c444 into main Mar 26, 2026
6 of 7 checks passed
@spa5k spa5k deleted the feat/native-execution-defaults branch March 26, 2026 09:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant