Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d7584b4
pm: stop global remove from leaking every bin it linked
colinhacks Aug 17, 2026
78e88f2
pm: stop a global install from replacing a binary it does not own
colinhacks Aug 17, 2026
ec6d0ec
pm: put global bins on a directory that is already on PATH
colinhacks Aug 17, 2026
060581f
install: stop re-adding the PATH block on every run
colinhacks Aug 17, 2026
99c9660
tests: add an end-to-end guard for global bin handling
colinhacks Aug 17, 2026
9ccb7a5
agents: aube tests DO run in CI, via aube-parity
colinhacks Aug 17, 2026
ab76aad
pm: put the global bin directory on PATH after a global install
colinhacks Aug 17, 2026
18afd40
docs: document global installs
colinhacks Aug 17, 2026
1952c6f
Merge remote-tracking branch 'origin/main' into global-bin-ownership
colinhacks Aug 19, 2026
627ec2d
Merge remote-tracking branch 'origin/main' into global-bin-ownership
colinhacks Aug 19, 2026
e46c57b
pm: fix three defects in the global bin ownership guard
colinhacks Aug 19, 2026
40b74cf
pm: key Windows bin ownership on the .cmd shim
colinhacks Aug 19, 2026
ea5775b
linker: bind the Windows shim parser to the writer it must agree with
colinhacks Aug 20, 2026
1cf6a0e
linker: one reader of the .cmd format, and a negative case for it
colinhacks Aug 20, 2026
9a7082a
linker: say what the npm-shim test actually pins
colinhacks Aug 20, 2026
7c044c3
Merge remote-tracking branch 'origin/main' into global-bin-ownership
colinhacks Aug 20, 2026
01beb15
pm/node: print the re-source hint when an upgrade rewrites the PATH line
colinhacks Aug 20, 2026
ad326c7
global: test the bin-slot policy on every platform, not just unix
colinhacks Aug 20, 2026
3e60620
pm: never truncate a shell profile, and hint the re-source on add -g
colinhacks Aug 20, 2026
38cccc5
version: retry the license-repair rename that Windows loses differently
colinhacks Aug 24, 2026
7fccb13
Merge remote-tracking branch 'origin/main' into global-bin-ownership
colinhacks Aug 24, 2026
a6c4fba
version: budget the contended-rename retry in time, not attempts
colinhacks Aug 24, 2026
05ff3e5
version: say that the rename budget covers one call, not a whole repair
colinhacks Aug 24, 2026
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
30 changes: 30 additions & 0 deletions crates/nub-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10339,6 +10339,21 @@ fn run_pm_shim_install() -> Result<i32> {
ProfileOutcome::AlreadyPresent(profile) => {
println!(" PATH: already present in {}", profile.display())
}
// Reached on upgrade. The directory is a compile-time constant within
// one build, but the CONSTANT ITSELF moved under XDG in #752, so a
// profile written by an older nub still names `$HOME/.nub/shims` beneath
// this marker and the line is rewritten in place. Only when that legacy
// directory is already GONE — the migration above strips the block
// outright while it still exists — so the live shell carries a
// directory on PATH that holds no shims, which is why the re-source
// hint matters more here than on a fresh add.
ProfileOutcome::Rewritten(profile) => println!(
" PATH: updated the entry in {} to point at {}\n \
restart your shell, or run: source {}",
profile.display(),
dir.display(),
profile.display()
),
// No writable profile for this shell: print the line and exit 0 (the
// spec's manual fallback — the shims themselves are installed).
ProfileOutcome::Manual { line } => println!(
Expand Down Expand Up @@ -10486,6 +10501,21 @@ fn run_node_shim_install() -> Result<i32> {
ProfileOutcome::AlreadyPresent(profile) => {
println!(" PATH: already present in {}", profile.display())
}
// Reached on upgrade. The directory is a compile-time constant within
// one build, but the CONSTANT ITSELF moved under XDG in #752, so a
// profile written by an older nub still names `$HOME/.nub/node-shim` beneath
// this marker and the line is rewritten in place. Only when that legacy
// directory is already GONE — the migration above strips the block
// outright while it still exists — so the live shell carries a
// directory on PATH that holds no shims, which is why the re-source
// hint matters more here than on a fresh add.
ProfileOutcome::Rewritten(profile) => println!(
" PATH: updated the entry in {} to point at {}\n \
restart your shell, or run: source {}",
profile.display(),
dir.display(),
profile.display()
),
ProfileOutcome::Manual { line } => println!(
" PATH: no known shell profile to edit — add this line to your shell config:\n {line}"
),
Expand Down
8 changes: 5 additions & 3 deletions crates/nub-cli/src/pm_engine/info_family.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
//! `whoami`/`owner`).
//! **Still a stub** (deliberately): `sbom` (below).
//!
//! `bin -g` / `root -g` print the engine's global-install layout (the
//! `PNPM_HOME`-compatible home, packages under its `global-aube/` subdir) —
//! real on-disk paths where the already-wired `add -g` installs, preserved
//! `bin -g` / `root -g` print the engine's global-install layout, and the two
//! now resolve from different roots: `bin -g` gives the SHARED user-binary
//! directory already on PATH (`~/.local/bin` and its `XDG_BIN_HOME`
//! relatives), while the installs themselves live under `<data>/<ns>/global`.
//! Real on-disk paths where the already-wired `add -g` installs, preserved
//! by the rewrite policy like the global-links residual in the install
//! family.
//!
Expand Down
84 changes: 84 additions & 0 deletions crates/nub-cli/src/pm_engine/install_family.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,85 @@ fn finish_code(result: miette::Result<Option<i32>>) -> Result<i32> {

// ───────────────────────── wired verbs ──────────────────────────

/// Put the global bin directory on PATH after a successful global install.
///
/// Installing a package globally has exactly one purpose — running it by name —
/// so an install that leaves the directory unreachable has not done what was
/// asked. The engine has already warned and named the line; this wires it.
///
/// Deliberately narrow. It runs ONLY on a successful global install, only when
/// the directory is genuinely absent from PATH (after the move to the shared
/// user-binary directory that is already true for few systems), and it writes
/// through the marker-keyed path so a profile can never collect a second copy
/// of the block. A failure here is reported and swallowed: the packages are
/// installed either way, and an unwritable profile must not fail the install.
fn wire_global_bin_path(code: i32) {
Comment thread
pullfrog[bot] marked this conversation as resolved.
if code != 0 {
return;
}
let Ok(layout) = aube::commands::global::GlobalLayout::resolve() else {
return;
};
if aube::commands::global::dir_is_on_path(&layout.bin_dir) {
return;
}
// Windows profile/registry editing is out of scope for v0 — the same call
// `nub pm shim` makes (cli.rs, `run_pm_shim_install`). Without this, the
// shell probe below finds no `SHELL`, falls back to the POSIX dialect, and
// writes `export PATH=…` into a `.profile` that neither cmd.exe nor
// PowerShell reads — then reports PATH configured. Printing the line is
// honest; the silent POSIX write is worse than either doing it properly or
// not doing it at all.
if cfg!(windows) {
eprintln!(
" PATH: add {} to your PATH (PATH editing isn't automated on Windows yet)",
layout.bin_dir.display()
);
return;
}
match nub_core::pm::shim::add_global_bin_path_block(&layout.bin_dir) {
Ok(nub_core::pm::shim::ProfileOutcome::Added(p)) => {
eprintln!(
" PATH: added {} to {}",
layout.bin_dir.display(),
p.display()
);
eprintln!(" Restart your shell, or source that file, to pick it up.");
}
Ok(nub_core::pm::shim::ProfileOutcome::Rewritten(p)) => {
eprintln!(
" PATH: updated the nub global bin entry in {} to {} — restart \
your shell, or source that file, to pick it up.",
p.display(),
layout.bin_dir.display()
);
}
Comment thread
pullfrog[bot] marked this conversation as resolved.
// The line is in a profile this shell reads, but the CURRENT shell has
// not sourced it — otherwise the on-PATH check above would have
// returned. Say what to do about the session in hand.
Ok(nub_core::pm::shim::ProfileOutcome::AlreadyPresent(p)) => {
eprintln!(
" PATH: {} is already configured in {} — restart your shell, \
or source that file, to pick it up.",
layout.bin_dir.display(),
p.display()
);
}
// No profile this shell reads could be written. This is the ONLY path
// that asks the user to do it by hand, which is why the engine no
// longer prints a remediation of its own.
Ok(nub_core::pm::shim::ProfileOutcome::Manual { line }) => {
eprintln!(
"warning: {} is not on PATH, so the commands just installed will \
not run.\n No shell profile could be written — add this line \
yourself:\n {line}",
layout.bin_dir.display()
);
}
Err(e) => eprintln!("warning: could not update your shell profile: {e}"),
}
}

fn run_add(typed: &str, args: &[String]) -> Result<i32> {
let (globals, verb): (_, aube::commands::add::AddArgs) = parse_or_return!(typed, args);
let session = super::engine_session(globals.dir.as_deref())?;
Expand All @@ -411,13 +490,18 @@ fn run_add(typed: &str, args: &[String]) -> Result<i32> {
&yarn_remedy("add", &verb.packages),
));
}
// Read before the move: `run` consumes `verb`.
let is_global = verb.global;
super::min_release_age::arm();
let code = finish_quieted(
&globals.output,
&session,
aube::commands::add::run(verb, globals.effective_filter()),
)?;
super::min_release_age::persist(&session.cwd, code == 0, &globals.output);
if is_global {
wire_global_bin_path(code);
}
stamp_if_virgin(&session, code);
crate::install_engine::record(&session.cwd, code);
// `nub add vite` (or adding any dep to a vite project) changes the graph;
Expand Down
12 changes: 7 additions & 5 deletions crates/nub-core/src/node/shim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,19 @@ use crate::pm::shim::{
/// from the PM shims' (`~/.nub/shims`, `# nub shims`) so the two install and
/// uninstall independently.
const NODE_SHIM_BLOCK: ShimBlock = ShimBlock {
marker: NODE_SHIM_MARKER,
marker: std::borrow::Cow::Borrowed(NODE_SHIM_MARKER),
// One descriptor, matching resolve_shim_dir's single rule. The default is
// spelled inline so a profile sourced without XDG_DATA_HOME still names a
// real dir, and `test -n` rather than fish's `set -q` because `set -q` is
// true for a DEFINED-but-empty variable (which would yield `/nub/node-shim`).
posix_line: r#"export PATH="${XDG_DATA_HOME:-$HOME/.local/share}/nub/node-shim:$PATH""#,
fish_line: concat!(
posix_line: std::borrow::Cow::Borrowed(
r#"export PATH="${XDG_DATA_HOME:-$HOME/.local/share}/nub/node-shim:$PATH""#,
),
fish_line: std::borrow::Cow::Borrowed(concat!(
"set -gx PATH (test -n \"$XDG_DATA_HOME\"; and echo $XDG_DATA_HOME; ",
"or echo $HOME/.local/share)/nub/node-shim $PATH"
),
dir_marker: NODE_SHIM_DIR_MARKER,
)),
dir_marker: std::borrow::Cow::Borrowed(NODE_SHIM_DIR_MARKER),
};

const NODE_SHIM_MARKER: &str = "# nub node shim";
Expand Down
Loading
Loading