Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ CI runs `cargo test`, `cargo clippy -- -D warnings`, `cargo fmt --check`, and a

## Project Structure
```
build.rs — cargo-install guard (cross-channel install detection via install-guard)
install-guard/ — shared install-channel detection + interactive uninstall prompt
src/
main.rs — CLI entry point (clap-based)
config.rs — Config load/save, root resolution
Expand Down
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ exclude = [
".tmp*",
]

[[bin]]
name = "numan"
path = "src/main.rs"
[build-dependencies]
numan-install-guard = { path = "install-guard", version = "0.1.0" }

[dependencies]
numan-install-guard = { path = "install-guard", version = "0.1.0" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔵 Trivial

Publish the guard before packaging numan-cli.

The path-plus-version dependency is suitable for local development, but a packaged numan-cli artifact requires numan-install-guard = 0.1.0 from the registry. Verify that the release pipeline publishes that exact guard version before the CLI package and publish stages. Otherwise the release pipeline can fail before producing the CLI artifact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Cargo.toml` around lines 23 - 26, Update the release pipeline around the
numan-cli publish/package flow to ensure numan-install-guard version 0.1.0 is
published to the registry before any CLI packaging or publish step runs. Keep
the local path-plus-version dependency in Cargo.toml for development, but make
the release ordering in the pipeline explicitly publish the guard first so the
packaged numan-cli can resolve numan-install-guard = 0.1.0 from the registry.

# CLI
clap = { version = "4", features = ["derive"] }
clap_complete = "4"
Expand Down Expand Up @@ -76,6 +76,10 @@ uuid = { version = "1", features = ["v7", "v4"] }
# Git (for source builds and registry sync)
git2 = "0.21"

[[bin]]
name = "numan"
path = "src/main.rs"

# stdio redirect for doctor --json repair passes (Unix only)
[target.'cfg(unix)'.dependencies]
libc = "0.2"
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,16 @@ brew tap tonythethompson/numan
brew install numan
```

Uses the public [`homebrew-numan`](https://github.com/tonythethompson/homebrew-numan) tap. Formula digests update automatically after each GitHub Release (see [docs/PACKAGING.md](docs/PACKAGING.md)).
Uses the public [`homebrew-numan`](https://github.com/tonythethompson/homebrew-numan) tap. Formula digests update automatically after each GitHub Release (see [docs/PACKAGING.md](docs/PACKAGING.md)). If you already have numan from cargo or winget, use `scripts/install-homebrew.sh` so the installer prompts to remove the other copy first.

### winget (Windows)

```powershell
winget install tonythethompson.numan
```

If you already have numan from **cargo** or **Homebrew**, use `scripts/install-winget.ps1` (winget) or `cargo install` (automatic guard) so the installer prompts to remove the other copy first.

See [packaging/winget/README.md](packaging/winget/README.md) and [docs/PACKAGING.md](docs/PACKAGING.md).

### crates.io
Expand All @@ -143,7 +145,7 @@ See [packaging/winget/README.md](packaging/winget/README.md) and [docs/PACKAGING
cargo install numan-cli
```

Requires [Rust](https://rustup.rs/) (stable). The installed binary is named `numan`.
Requires [Rust](https://rustup.rs/) (stable). The installed binary is named `numan`. During `cargo install`, numan checks for winget/Homebrew/release copies and prompts to uninstall them first; decline cancels the install.

**Requirements:** a [Nushell](https://www.nushell.sh/) binary on `PATH` for `numan init`, `numan activate`, and related commands.

Expand Down
7 changes: 7 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fn main() {
if std::env::var("CARGO_INSTALL_ROOT").is_ok()
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
&& numan_install_guard::run_cargo_install_guard() != std::process::ExitCode::SUCCESS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Run the cargo guard during plain installs

For normal cargo install numan-cli or cargo install --git ..., Cargo does not set CARGO_INSTALL_ROOT for build scripts unless the user explicitly exports it; I verified with a minimal cargo install --root ... -vv probe that the build script saw CARGO_INSTALL_ROOT=None. This condition therefore skips the advertised automatic guard on the default cargo install path, allowing cross-channel installs to proceed silently.

AGENTS.md reference: AGENTS.md:L37-L38

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files relevant =="
git ls-files | rg '(^build\.rs$|AGENTS\.md$|README\.md$|docs/PACKAGING\.md$|packaging/winget/README\.md$|^Cargo\.toml$|install-guard|numan_install_guard)' || true

echo
echo "== build.rs =="
cat -n build.rs

echo
echo "== relevant docs snippets =="
for f in AGENTS.md README.md docs/PACKAGING.md packaging/winget/README.md Cargo.toml; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f" | sed -n '1,220p'
  fi
done

echo
echo "== references to CARGO_INSTALL_ROOT / cargo install guard =="
rg -n "CARGO_INSTALL_ROOT|cargo install|install-guard|guard|uninstall|winget|Homebrew" .

Repository: tonythethompson/numan

Length of output: 50379


🏁 Script executed (no clone):

#!/bin/bash
set -euo pipefail

echo "== cargo reference for CARGO_INSTALL_ROOT via rustforge/cargo-source =="
curl -fsSL 'https://raw.githubusercontent.com/rust-lang/cargo/master/src/cargo/util/config/mod.rs' \
  | rg -n "CARGO_INSTALL_ROOT|install\.root" -C 4 || true

echo
echo "== cargo docs pages (raw) for CARGO_INSTALL_ROOT =="
curl -fsSL 'https://raw.githubusercontent.com/rust-lang/cargo/master/src/doc/reference/environment-variables.adoc' \
  | rg -n "CARGO_INSTALL_ROOT|install\.root" -C 6 || true

Length of output: 348


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== install-guard public API =="
cat -n install-guard/src/lib.rs | sed -n '1,260p'

echo
echo "== install-guard CLI behavior =="
cat -n install-guard/src/main.rs | sed -n '1,260p'

echo
echo "== cargo wrapper scripts =="
for f in scripts/install-cargo.ps1 scripts/install-homebrew.sh scripts/install-winget.ps1; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    cat -n "$f" | sed -n '1,220p'
  fi
done

Repository: tonythethompson/numan

Length of output: 14109


🌐 Web query:

Cargo reference CARGO_INSTALL_ROOT install.root environment variable documentation

💡 Result:

The CARGO_INSTALL_ROOT environment variable is used to define the root directory where the cargo install command places installed binary crates [1][2]. Executables are specifically placed into a bin subdirectory under this root path [1][2]. Additionally, metadata files used to track installed packages, such as.crates.toml and.crates2.json, are stored within this root directory [1][2]. If not specified, the default installation root is Cargo's home directory, which typically defaults to.cargo in the user's home directory [1][2]. The precedence for determining the installation root, from highest to lowest, is as follows [3][4]: 1. The --root command-line option [3][1] 2. The CARGO_INSTALL_ROOT environment variable [3][1] 3. The install.root Cargo configuration value [3][1] 4. The CARGO_HOME environment variable [3][4] 5. The default location ($HOME/.cargo) [3][4] The install.root configuration value mentioned in Cargo's TOML configuration is equivalent to the CARGO_INSTALL_ROOT environment variable [1][2]. Cargo configuration keys formatted as foo.bar can generally be defined by setting an environment variable of the form CARGO_FOO_BAR [1][2]. In this case, the install.root key corresponds to CARGO_INSTALL_ROOT [1][2].

Citations:


Use an install-only guard boundary.

CARGO_INSTALL_ROOT sets Cargo’s install root path and is resolved after resolving the default .cargo root if absent. It does not prove the current invocation is cargo install, so build.rs can also run the uninstall prompt for configured cargo build/cargo test invocations, while standard cargo install numan-cli without --root may skip it. Gate this with an install-only signal or wrapper instead of treating CARGO_INSTALL_ROOT as the installation boundary.

Update the docs that describe plain cargo install numan-cli as automatically enforcing this guard: AGENTS.md#37, README.md#148, docs/PACKAGING.md#49, and packaging/winget/README.md#29.

📍 Affects 5 files
  • build.rs#L2-L3 (this comment)
  • AGENTS.md#L37-L37
  • README.md#L148-L148
  • docs/PACKAGING.md#L49-L49
  • packaging/winget/README.md#L29-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@build.rs` around lines 2 - 3, Replace the CARGO_INSTALL_ROOT check in
build.rs with a reliable install-only signal or wrapper so
run_cargo_install_guard executes only for cargo install invocations and still
covers standard cargo install numan-cli without --root. Update AGENTS.md line
37, README.md line 148, docs/PACKAGING.md line 49, and
packaging/winget/README.md line 29 to remove or correct claims that plain cargo
install numan-cli automatically enforces the guard.

{
std::process::exit(1);
}
}
18 changes: 15 additions & 3 deletions docs/PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,22 @@ python3 scripts/render_homebrew_formula.py --version X.Y.Z --sha256sums SHA256SU
| GitHub Release | Download archive from [Releases](https://github.com/tonythethompson/numan/releases) |
| crates.io | `cargo install numan-cli` |
| From git | `cargo install --git https://github.com/tonythethompson/numan` |
| Homebrew (tap) | `brew tap tonythethompson/numan && brew install numan` |
| winget (community) | `winget install tonythethompson.numan` |
| Homebrew tap | `brew tap tonythethompson/numan && brew install numan` (use `scripts/install-homebrew.sh` when switching from cargo/winget) |
| winget (community) | `winget install tonythethompson.numan` (use `scripts/install-winget.ps1` when switching from cargo/Homebrew) |

## Install channel guard

Cross-channel installs (cargo vs winget vs Homebrew) prompt to uninstall the existing copy first; declining cancels the install.

| Channel | Guard behavior |
|---------|----------------|
| `cargo install numan-cli` | Automatic via `build.rs` during install |
| winget | `powershell -File scripts/install-winget.ps1` |
| Homebrew tap | `bash scripts/install-homebrew.sh` |
| CI / automation | Set `NUMAN_SKIP_INSTALL_GUARD=1` to bypass |

`numan doctor` warns when multiple channels are detected (`install.multiple_channels`).

## Archive layout

Release archives extract to `numan-<version>-<target>/` containing the `numan`
(or `numan.exe`) binary. Homebrew and winget installers assume this layout.
Expand Down
1 change: 1 addition & 0 deletions docs/numan-doctor.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Checks run in order below. Implementation should call existing validators (`NuPa
|----|----------|-----------|
| `nu.binary.missing_on_path` | `error` | Nu not on PATH and not under `$NUMAN_ROOT/tools/nushell/` → fix: `numan setup nu` |
| `nu.binary.found_off_path` | `warn` | Nu exists in a known install root (e.g. `~/.cargo/bin`, `%LOCALAPPDATA%\Programs\nushell`) but not on PATH → fix: `numan setup nu use <path>` |
| `install.multiple_channels` | `warn` | More than one `numan` binary from different install channels (cargo, winget, homebrew, release archive) → fix: uninstall the other channel(s) before reinstalling |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (typo): Capitalize "Homebrew" to match its usage elsewhere and maintain consistency in tool names.

Update "homebrew" here to "Homebrew" so it matches the capitalization used elsewhere in the docs.

Suggested change
| `install.multiple_channels` | `warn` | More than one `numan` binary from different install channels (cargo, winget, homebrew, release archive) → fix: uninstall the other channel(s) before reinstalling |
| `install.multiple_channels` | `warn` | More than one `numan` binary from different install channels (cargo, winget, Homebrew, release archive) → fix: uninstall the other channel(s) before reinstalling |

Fix in Cursor

| `nu.path.version` | `info` | PATH-only Nu version (`PATH Nu: 0.114.1`), `PATH Nu: not found`, or `PATH Nu: found at '<path>' but version probe failed (<error>)` when the binary exists but `--version` fails. Does not treat managed Nu as PATH. Report-only (no automatic repair). |
| `nu.managed.version` | `info` | Managed binary under `$NUMAN_ROOT/tools/nushell/` with version, `Managed Nu: not installed`, or `Managed Nu: present at '<path>' but version probe failed (<error>)` when the binary exists but `--version` fails. Report-only (no automatic repair). |
| `nu.active_version.invalid` | `error` | `nu_state/active-version.json` is present but unreadable/invalid JSON. Lookup would otherwise soft-miss the marker and fall back to PATH. **auto:** copy raw bytes to `active-version.json.corrupt` (best-effort, recoverable `binary_path`), then clear via `clear_active_version` so resolution recovers cleanly. |
Expand Down
11 changes: 11 additions & 0 deletions install-guard/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "numan-install-guard"
version = "0.1.0"
edition = "2021"
publish = true

[[bin]]
name = "numan-install-guard"
path = "src/main.rs"

[dependencies]
Loading
Loading