Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ src/
completions.rs — `numan completions <shell>`: install by default (mkdir+write); `--print` for stdout (Phase 7.3)
setup.rs — `numan setup nu [VERSION]|remove|path|use <path>` + `setup loader`: Nushell bootstrap + nushell-loader install
try_cmd.rs — `numan try [--yes] [--no-activate]`: curated starter install + activate for current Nu
use_cmd.rs — `numan use <version>|latest|list`: activates a previously installed managed Nu version (no auto-download); writes the active-version marker after a PreMutation snapshot under the root mutation lock
use_cmd.rs — `numan use <version>|latest|list`: activates a previously installed managed Nu version (no auto-download); cross-minor leave/teardown (modules then plugins) + restore (plugins then modules) via activation profiles; same-target is restore-only; writes the active-version marker after a PreMutation snapshot under the root mutation lock
activation_switch.rs — shared leave/restore orchestration for `numan use` (lower-level lifecycle, no profile-sync wrappers)
nu_pin_offer.rs — Shared TTY offer to `setup nu <version>` + `init --refresh` on Nu mismatch
install/
download.rs — HTTP download with progress
Expand All @@ -84,6 +85,7 @@ src/
migration_journal.rs — `state/migration-journal.json` for legacy-Nu single-binary → versioned-layout transition (Prepared → Renamed → Active stages); self-heal at top of `migrate_legacy_install_with_detector`, reconciled by `numan doctor --fix` (auto-tier repair)
snapshot.rs — Immutable activation snapshots (`create_snapshot`, `list_snapshots`, etc.)
rollback.rs — Journaled restore of Numan-owned state to a snapshot
activation_profile.rs — Desired per-Nu-minor activation sets (`nu_state/activation-profile.json`); leave union; user activate/deactivate/remove sync
nupm_import.rs — nupm-import provenance (`state/nupm-imports.json`, Phase 6.2)
nu/
bootstrap.rs — download/install official Nushell release under tools/nushell
Expand Down Expand Up @@ -147,6 +149,7 @@ tests/
- **Plugin deactivate journal**: `state/pending-plugin-deactivate.json` (`Prepared` → `Unregistered` → clear lockfile `activation`); reconciled on next `deactivate`; doctor warns `journal.plugin_deactivate_pending`
- **Migration journal**: `state/migration-journal.json` for the legacy-Nu single-binary → versioned layout transition. Stages `Prepared` (before `create_dir_all`) → `Renamed` (after legitimate `rename`) → `Active` (after `write_active_version`); journal deleted on transition to `Active`. Every well-formed pending journal stage (`Prepared`, `Renamed`, and `Active`) is reconciled by `numan doctor --fix` (Auto-tier, fix hint `numan use`) and by the self-healing `reconcile(root)?` at the top of every `migrate_legacy_install_with_detector` call; file-system truth takes precedence over journal stage when they disagree. Unreadable or schema-mismatched journals emit `journal.migration_invalid` (Error severity, Manual repair tier: delete the stale journal); they are not auto-reconciled. `reconcile` refuses to act when `tools/nushell` is a symlink or reparse point (`assert_not_symlink` guard); the journal is left unchanged on that path so a follow-up attempt can succeed once the symlink is resolved. A `Prepared`-stage orphan directory that cannot be removed (e.g. ENOTEMPTY) causes `reconcile` to return `Err` and retain the journal so the next invocation can retry.
- **Active version marker**: `nu_state/active-version.json` (`{ "version": "X.Y.Z" }`, optionally `{ "version": "X.Y.Z", "binary_path": "/abs/path/to/nu" }` for off-tree selections). Sole authority for which `tools/nushell/<v>/` is selected. Written by `numan setup nu` and `numan use <version>|latest`. The optional `binary_path` records the resolved off-tree binary when `numan setup nu use <path>` swaps to a user-supplied Nu so subsequent `numan use list` and `find_nu_executable_with_root` can resolve the chosen version even when no on-tree install exists (the field uses `#[serde(default, skip_serializing_if = "Option::is_none")]` so the on-disk shape stays `{ "version": ... }` for on-tree selections and pre-existing markers still load).
- **Activation profile**: `nu_state/activation-profile.json` stores desired per-Nu-minor plugin/module sets. Cross-minor `numan use` unions currently Numan-active packages into the leaving minor (never shrinks), tears down modules then plugins, switches the marker, then restores the target minor (plugins then modules). Same-target `use` only reconciles missing desired activations. User `activate`/`deactivate` are idempotent desired-state ops on the current minor; `remove` deletes the id from all minors. `numan use` calls lifecycle beneath profile-sync wrappers so leave/restore do not wipe saved desire.
- **Atomic writes**: all JSON state files (lockfile, journal, nu_state/paths.json) use `write_json_atomic` (tempfile in same dir + persist) — no partial-write corruption
- **Function signatures**: use `&Path` not `&PathBuf` in function parameters (clippy::ptr_arg is CI-enforced)

Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- **Release assets** for `aarch64-unknown-linux-gnu` and `aarch64-pc-windows-msvc` (native ARM runners); `numan update --self` maps those triples
- **Homebrew** Linux ARM archive support; **winget** verifies and submits both Windows x64 and ARM64 zips
- **`numan use` activation profiles**: cross-minor switches auto-deactivate Numan-active plugins/modules (leave profile is a never-shrinking union per Nu minor in `nu_state/activation-profile.json`) and restore the target minor's desired set after the marker write. Same-target `use` is restore-only reconcile. User `activate`/`deactivate` keep the current minor's desired set in sync (idempotent even when already active/inactive); `remove` clears the package id from every minor.

### Changed

- **`numan completions <shell>`** installs by default (creates the target directory if needed). Use `--print` to emit the script on stdout for piping or custom redirects.
- **`numan registry packages`**: clearer listing (blank line between entries, styled id/version/type, soft-wrapped dim descriptions)

### Fixed

Expand All @@ -38,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Then open a new terminal and confirm with `$env:Path -split ';'`.

- **`numan doctor`**: report versioned managed Nu installs (`tools/nushell/<version>/nu`) instead of only the legacy `tools/nushell/nu` path
- **`numan setup nu`**: official Nushell 0.114.x release archives exceed the old
256 MiB extract cap (~279 MiB uncompressed on linux-gnu). Bootstrap now
extracts only the `nu` binary (skipping bundled plugins) and raises the
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,8 @@ Global flag: `--root <path>` — override the numan root directory (all commands
| `numan setup nu remove` | Remove the managed Nushell install and fall back to PATH Nu |
| `numan setup nu path` | Use the Nushell already on PATH (removes managed install) |
| `numan setup nu use <path>` | Register a specific existing Nushell binary |
| `numan use <version>` | Switch the active managed Nu to a pinned version (no auto-install; errors with a hint to run `numan setup nu <version>` if missing) |
| `numan use latest` | Switch the active managed Nu to the latest installed version |
| `numan use <version>` | Switch the active managed Nu to a pinned version (no auto-install; errors with a hint to run `numan setup nu <version>` if missing). Cross-minor switches deactivate Numan-active plugins/modules for the leaving Nu and restore that minor's remembered set when you switch back. |
| `numan use latest` | Switch the active managed Nu to the latest installed version (same leave/restore behavior as `use <version>`) |
| `numan use list` | List installed managed Nu versions and mark the active one |
| `numan nupm status` | Summarize nupm home and import eligibility |
| `numan nupm inspect [--all] [path]` | Classify nupm packages at a path |
Expand Down
68 changes: 68 additions & 0 deletions src/cmd/activate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ fn execute_with_registrar_and_runner(
// These are already caught in resolve functions.

if plugin_targets.is_empty() && module_targets.is_empty() {
// Already-active (or inactive-but-requested) packages still need profile sync.
sync_user_activate_profile(root, &nu_paths, args, &lockfile)?;
println!("Nothing to activate.");
return Ok(());
}
Expand Down Expand Up @@ -177,6 +179,7 @@ fn execute_with_registrar_and_runner(
let module_targets = resolve_module_targets(args, &lockfile, &nu_paths)?;

if plugin_targets.is_empty() && module_targets.is_empty() {
sync_user_activate_profile(root, &nu_paths, args, &lockfile)?;
println!("Nothing to activate.");
return Ok(());
}
Expand Down Expand Up @@ -238,6 +241,10 @@ fn execute_with_registrar_and_runner(
}
}

// Reload lockfile so profile sync sees post-activation state for "activate all".
let lockfile = Lockfile::load(root)?;
sync_user_activate_profile(root, &nu_paths, args, &lockfile)?;

if any_failed {
bail!(
"One or more packages failed to activate. Successful activations have been persisted."
Expand All @@ -247,6 +254,28 @@ fn execute_with_registrar_and_runner(
Ok(())
}

fn sync_user_activate_profile(
root: &Path,
nu_paths: &NuPaths,
args: &ActivateArgs,
lockfile: &Lockfile,
) -> Result<()> {
let ids = if args.packages.is_empty() {
let active = crate::cmd::activation_switch::collect_currently_active(lockfile, nu_paths);
let mut all = active.plugins;
all.extend(active.modules);
all
} else {
args.packages.clone()
};
crate::cmd::activation_switch::sync_profile_after_user_activate(
root,
&nu_paths.nu_version,
&ids,
lockfile,
)
}

// ── Plugin lane ────────────────────────────────────────────────────────────────

/// Run the plugin registration lane. Returns `true` if any plugin failed.
Expand Down Expand Up @@ -569,6 +598,45 @@ fn run_module_lane(
Ok(false) // lane succeeded
}

/// Activate named modules without acquiring the mutation lock, snapshotting, or
/// syncing the activation profile. Caller must hold the root mutation lock.
///
/// Returns `true` if the module lane reported failure.
pub(crate) fn activate_modules_unlocked(
root: &Path,
package_ids: &[String],
runner: &dyn CandidateRunner,
) -> Result<bool> {
if package_ids.is_empty() {
return Ok(false);
}

let nu_paths = NuPaths::load(root)?;
nu_paths.validate_drift()?;
let mut lockfile = Lockfile::load(root)?;

let args = ActivateArgs {
packages: package_ids.to_vec(),
verbose: false,
list: false,
check: false,
};
let module_targets = resolve_module_targets(&args, &lockfile, &nu_paths)?;
if module_targets.is_empty() {
return Ok(false);
}
let managed_file_path = resolve_managed_file_path(&nu_paths)?;
run_module_lane(
root,
&nu_paths,
&mut lockfile,
&module_targets,
&managed_file_path,
runner,
None,
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// ── --list subcommand ──────────────────────────────────────────────────────────

fn execute_list(lockfile: &Lockfile, nu_paths: &NuPaths) -> Result<()> {
Expand Down
Loading
Loading