Skip to content

feat(embed): let hosts supply the node runtime for lifecycle scripts - #1079

Merged
jdx merged 4 commits into
mainfrom
embed-node-runtime
Jul 21, 2026
Merged

jdx merged 4 commits into
mainfrom
embed-node-runtime

Conversation

@jdx

@jdx jdx commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds a way for a host embedding aube (e.g. mise) to supply the node runtime that dependency lifecycle scripts run on, instead of aube falling back to an ambient node PATH lookup.

Motivation: mise embeds aube::embed::add to install npm: tools (jdx/mise#11149) with node managed as a mise dependency — not on the process PATH. Today runtime::node_program() falls back to PathBuf::from("node") when no runtime switch is active, so allow_builds installs whose dependencies run lifecycle scripts fail to find node. The old subprocess path (aube add --global) avoided this by prepend_path-ing the dependency toolset; this restores that for the in-process path.

Change

  • embed::InstallOptions and AddToProjectOptions gain node_bin_dir: Option<PathBuf> (the dir containing the host's node). Threads into install::InstallOptions::embedder_node_bin_dir.
  • Inside install::run — within the runtime scope, before runtime::ensure — runtime::seed_embedder_node(bin_dir) sets the scoped runtime slot to a RuntimeContext pointing at the host's node (source: Embedder, provenance: Mise). ensure returns early when the slot is populated, so aube does no runtime resolution of its own and node_program() / path_entries() return the host's node.
  • Safe under concurrent installs: the runtime slot is a tokio::task_local, scoped per-install — no process-global mutation.
  • Behavior-preserving when unset (None): normal runtime switching / PATH fallback.

Tests

  • New runtime::tests::seed_embedder_node_drives_node_program_and_path — within a scope, the seed drives node_program() and path_entries(), and is idempotent (a second seed doesn't clobber, matching ensure's early return).
  • seed_embedder_node_is_a_noop_outside_scope — no scope → no-op, no panic.
  • Full cargo test -p aube --lib (667) passes; clippy clean.

This PR was generated by an AI coding assistant (Claude Code).


Note

Medium Risk
Changes how lifecycle scripts resolve Node for embedders that opt in; scoped per-install via task-local state, but incorrect node_bin_dir could break builds or version checks for those hosts.

Overview
Adds node_bin_dir on embed APIs (embed::InstallOptions, AddToProjectOptions) and embedder_node_bin_dir on install::InstallOptions, wired through embed::install / add_to_project.

Before runtime::ensure, install calls runtime::seed_embedder_node, which fills the per-install task-local runtime slot with the host’s node (RuntimeSource::Embedder), prepends that bin dir to script PATH, and probes --version so engine checks match the same binary. ensure then skips its own resolution when the slot is already set.

CLI, CI, deploy, FFI, and Node-API callers keep None (unchanged behavior). New unit tests cover seeding inside an install scope and no-op behavior outside it.

Reviewed by Cursor Bugbot for commit df3e68e. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Embedded installations can run lifecycle scripts using a host-provided node executable directory.
    • Lifecycle script runtime selection is now scoped to the installation for consistent node resolution.
  • Bug Fixes

    • Prevents embedded flows from unnecessarily resolving/downloading a different Node.js runtime.
  • Compatibility

    • Default behavior remains unchanged when no host node directory is provided across CLI, CI, deployment, and FFI paths.
  • Tests

    • Added coverage for scoped Node seeding behavior and ensuring it doesn’t leak outside the install scope.

Adds `node_bin_dir` to the embed install/add options. When set, the
install seeds its scoped runtime slot (runtime::seed_embedder_node) with
that node before `ensure` runs, so lifecycle scripts spawn on the host's
node and find it on PATH — instead of falling back to an ambient `node`
lookup. Threads through AddToProjectOptions and install::InstallOptions;
no-op and behavior-preserving when unset.

This lets an embedder (mise) that manages its own node as a dependency
run `allow_builds` lifecycle scripts without requiring node on the
process PATH — restoring what the subprocess `aube add --global` path
provided via prepend_path.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 17:28
@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds an optional embedder-provided Node binary directory to install options, propagates it through embedded add/install flows, and seeds the scoped runtime for lifecycle scripts. Standard CLI, CI, and deploy paths default the option to None.

Changes

Embedder Node Runtime

Layer / File(s) Summary
Install option contracts and defaults
crates/aube/src/commands/install/args.rs, crates/aube/src/commands/ci.rs, crates/aube/src/commands/deploy/mod.rs, crates/aube/src/embed.rs
Install options define the optional Node binary directory; standard constructors and command paths initialize it to None.
Scoped embedder runtime seeding
crates/aube/src/runtime.rs, crates/aube/src/commands/install/mod.rs
A scoped embedder runtime uses the provided node or node.exe, preserves the first seed, and is selected before normal runtime initialization.
Embedded install propagation
crates/aube/src/commands/add/mod.rs, crates/aube/src/embed.rs, crates/aube-ffi/src/lib.rs, crates/aube-node/src/lib.rs
Embedded add and install APIs forward the configured Node directory, while FFI and Node bindings explicitly use None.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Embedder
  participant InstallOptions
  participant run_inner
  participant RuntimeScope
  Embedder->>InstallOptions: provide embedder_node_bin_dir
  InstallOptions->>run_inner: forward Node binary directory
  run_inner->>RuntimeScope: seed_embedder_node
  RuntimeScope-->>run_inner: lifecycle scripts use seeded node
Loading

Suggested reviewers: copilot

Poem

A rabbit hops with Node in tow,
Through scoped paths where scripts can go.
The first seed stays, the defaults rest,
Embedded installs now know the best.
None keeps old flows bright and blessed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: embedder-hosted Node runtime support for lifecycle scripts.

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c4edac8. Configure here.

Comment thread crates/aube/src/runtime.rs
@greptile-apps

greptile-apps Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR lets embedding hosts provide the Node.js runtime used by lifecycle scripts. The main changes are:

  • Adds an optional Node binary directory to the embed install and add APIs.
  • Seeds the per-install runtime context before normal runtime resolution.
  • Normalizes relative runtime directories before lifecycle scripts use them.
  • Updates CLI, FFI, and Node binding option constructors with safe defaults.
  • Adds tests for scoped runtime seeding and isolation.

Confidence Score: 5/5

This looks safe to merge.

  • All exhaustive option literals reviewed now initialize the new fields.
  • Relative runtime directories are converted to absolute paths before use.
  • Runtime seeding occurs inside the install scope and before normal resolution.
  • No blocking issues remain in the changed code.

Important Files Changed

Filename Overview
crates/aube/src/runtime.rs Adds scoped host-runtime seeding, absolute path normalization, version probing, and focused tests.
crates/aube/src/commands/install/mod.rs Seeds the host runtime inside the install scope before normal runtime resolution.
crates/aube/src/commands/install/args.rs Adds the internal embedder runtime option and initializes it to None for standard installs.
crates/aube/src/commands/add/mod.rs Propagates the optional host Node directory from add operations into install options.
crates/aube/src/embed.rs Exposes the optional Node directory through the public embedding install API.
crates/aube-node/src/lib.rs Updates the Node binding option literal with the new default value.
crates/aube-ffi/src/lib.rs Updates the FFI option literal with the new default value.

Reviews (4): Last reviewed commit: "fix(embed): make seed_embedder_node asyn..." | Re-trigger Greptile

Comment thread crates/aube/src/commands/add/mod.rs Outdated
Comment thread crates/aube/src/runtime.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR adds an embedder-provided Node.js runtime override so embedded aube installs can run dependency lifecycle scripts using a host-managed node (instead of relying on an ambient PATH lookup), by seeding a per-install task-local runtime context before runtime resolution occurs.

Changes:

  • Adds node_bin_dir: Option<PathBuf> to the embed API install options and threads it into install wiring as embedder_node_bin_dir.
  • Introduces runtime::seed_embedder_node() to populate the install-scoped runtime slot so runtime::ensure() short-circuits and script spawning uses the host’s Node.
  • Updates internal install/add entry points and constructors to carry the new option, and adds unit tests for the seeding behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
crates/aube/src/runtime.rs Adds RuntimeSource::Embedder, implements seed_embedder_node, and adds unit tests around seeding behavior.
crates/aube/src/embed.rs Exposes node_bin_dir on the embed install API and forwards it into internal install options.
crates/aube/src/commands/install/mod.rs Seeds the embedder node dir into the runtime slot before calling runtime::ensure.
crates/aube/src/commands/install/args.rs Extends internal InstallOptions with embedder_node_bin_dir and initializes defaults.
crates/aube/src/commands/deploy/mod.rs Updates InstallOptions construction to include the new field.
crates/aube/src/commands/ci.rs Updates InstallOptions construction to include the new field.
crates/aube/src/commands/add/mod.rs Adds the embedder-facing option on AddToProjectOptions and forwards it into install options.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/aube/src/commands/add/mod.rs Outdated
Comment on lines +48 to +52
/// Directory containing the `node` executable an embedding host wants
/// lifecycle scripts to run on (see
/// [`crate::commands::install::InstallOptions::embedder_node_bin_dir`]).
/// `None` keeps aube's own runtime resolution.
pub embedder_node_bin_dir: Option<std::path::PathBuf>,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 1352d26 — the public AddToProjectOptions field is now node_bin_dir, matching embed::InstallOptions. The internal install::InstallOptions.embedder_node_bin_dir keeps its name (it's the plumbing target, not embedder-facing).

This reply was generated by an AI coding assistant.

Comment thread crates/aube/src/commands/add/mod.rs Outdated
Comment on lines +809 to +814
#[tokio::test]
async fn seed_embedder_node_is_a_noop_outside_scope() {
// No install scope active → nothing to seed, and no panic.
seed_embedder_node(PathBuf::from("/opt/mise/node/bin"));
assert!(current().is_none());
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch — fixed in 1352d26. The test now asserts inside a fresh scope (reads the empty task-local slot) instead of calling current() bare (which falls back to the process-wide RUNTIME), so it's no longer order-dependent.

This reply was generated by an AI coding assistant.

- fix aube-node / aube-ffi AddToProjectOptions literals that omitted the
  new field (workspace bindings failed to compile)
- name the public option node_bin_dir on AddToProjectOptions too, matching
  embed::InstallOptions (was embedder_node_bin_dir)
- absolutize the supplied bin dir so scripts run with a different cwd still
  resolve node + the prepended PATH entry
- probe the supplied node for its version so engine checks and
  virtual-store hashing key off the same node as lifecycle scripts
- make the outside-scope test assert via a fresh scope (task-local),
  not the process-wide RUNTIME cell, to avoid order-dependence

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 17:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread crates/aube/src/runtime.rs Outdated
Comment on lines +144 to +151
pub fn seed_embedder_node(bin_dir: PathBuf) {
// Absolutize: lifecycle scripts may run with a different working
// directory, so both `node_program()` and the prepended PATH entry must
// resolve independently of cwd. `absolute` doesn't require the dir to
// exist or touch symlinks; fall back to the input if it errors.
let bin_dir = std::path::absolute(&bin_dir).unwrap_or(bin_dir);
let node_exe = if cfg!(windows) { "node.exe" } else { "node" };
let node_bin = bin_dir.join(node_exe);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in df3e68e. seed_embedder_node now checks the runtime slot first and returns before any absolute/version-probe work when it's outside a scope or the slot is already seeded.

This reply was generated by an AI coding assistant.

Comment thread crates/aube/src/runtime.rs Outdated
Comment on lines +807 to +816
let bin_dir = PathBuf::from("/opt/mise/node/bin");
seed_embedder_node(bin_dir.clone());

let ctx = current().expect("slot seeded");
assert_eq!(ctx.source, RuntimeSource::Embedder);
assert_eq!(ctx.bin_dir.as_deref(), Some(bin_dir.as_path()));

let node_exe = if cfg!(windows) { "node.exe" } else { "node" };
assert_eq!(node_program(), bin_dir.join(node_exe));
assert_eq!(path_entries(), vec![bin_dir.clone()]);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Already handled in 7b95d66 (before this comment's commit): the test computes its expected via std::path::absolute too, so it matches the seed's own normalization on Windows. windows-latest is green.

This reply was generated by an AI coding assistant.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 17:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

crates/aube/src/runtime.rs:149

  • seed_embedder_node documents that it is a no-op outside an install scope() and when the slot is already populated, but it currently still does work (including a node --version spawn) before discovering it can't/shouldn't set the task-local slot.

Also, if the supplied bin_dir doesn't actually contain node, the function still seeds a context pointing at a non-existent binary, which causes later spawns to fail and prevents runtime::ensure() from doing normal resolution.

This comment was generated by GitHub Copilot.

    // Absolutize: lifecycle scripts may run with a different working
    // directory, so both `node_program()` and the prepended PATH entry must
    // resolve independently of cwd. `absolute` doesn't require the dir to
    // exist or touch symlinks; fall back to the input if it errors.
    let bin_dir = std::path::absolute(&bin_dir).unwrap_or(bin_dir);

options.dangerously_allow_all_builds,
);
install_options.control = options.control;
install_options.embedder_node_bin_dir = options.node_bin_dir;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This compiles — a partial move is fine here. node_bin_dir is the last use of that field, and the subsequent options.offline (a Copy bool) reads a different, still-initialized field. options.control is moved on the line just above for the same reason. Confirmed by green cargo build --workspace / clippy CI.

This reply was generated by an AI coding assistant.

Comment on lines +603 to +605
if let Some(bin_dir) = &opts.embedder_node_bin_dir {
crate::runtime::seed_embedder_node(bin_dir.clone());
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in df3e68e. seed_embedder_node is now async and probes node --version via tokio::process::Command, so the install task no longer blocks a Tokio worker on the child spawn. The call site in install::run awaits it.

This reply was generated by an AI coding assistant.

- check the runtime slot up front and return before any path/version probing
  when outside a scope or already seeded
- probe node --version via tokio::process so the install task doesn't block a
  Tokio worker on the child spawn

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 21, 2026 19:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment on lines +175 to +183
let ctx = RuntimeContext {
node_bin: Some(node_bin),
bin_dir: Some(bin_dir),
version,
requested: None,
source: RuntimeSource::Embedder,
provenance: RuntimeProvenance::Mise,
fresh_pin: None,
};
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.

2 participants