Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
9fdd94c
refactor(setup nu): replace action flags with subcommands + shared co…
tonythethompson Aug 1, 2026
633c50c
fix: 3 findings — Prevent PATH subcommand from deleting active managed N
qodo-code-review[bot] Aug 1, 2026
fb0ff23
fix: address PR #66 review feedback
tonythethompson Aug 1, 2026
438d80b
fix: clarify PATH messaging in Nu setup hints
tonythethompson Aug 1, 2026
7e726a2
Add reserved `numan use` CLI stub
tonythethompson Aug 1, 2026
7951797
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] Aug 1, 2026
8062a11
feat: implement numan use for side-by-side Nu version management
tonythethompson Aug 1, 2026
213e874
fix: 5 findings — Validate and normalize Nu versions; Propagate legacy m
qodo-code-review[bot] Aug 1, 2026
b8f4233
fix: address PR review — validate before delete, fix help text, fix c…
tonythethompson Aug 1, 2026
62b8656
chore: remove temp files
tonythethompson Aug 1, 2026
2591036
Update src/cmd/snapshot.rs
tonythethompson Aug 2, 2026
d9b8e0f
fix: PR 67 review - bootstrap yes flag, version check, mutation lock
tonythethompson Aug 2, 2026
0429557
Merge origin/master into feature/numan-use - resolve conflicts: take …
tonythethompson Aug 2, 2026
e245232
Update .gitignore
tonythethompson Aug 2, 2026
641bd24
wip(numan-use): integrate use + migration pieces (split pending)
tonythethompson Aug 2, 2026
e0e467e
feat(nu): extract legacy migrate fs + tests into src/nu/migrate_legacy
tonythethompson Aug 2, 2026
a006204
fix: Reconcile migration journal under lock
qodo-code-review[bot] Aug 2, 2026
10b4e31
fix: Recover prepared migrations from filesystem state
qodo-code-review[bot] Aug 2, 2026
7153a7f
fix: Remove redundant migration comment
qodo-code-review[bot] Aug 2, 2026
283a1c4
fix: Keep use list read-only
qodo-code-review[bot] Aug 2, 2026
45846fc
Potential fix for pull request finding
tonythethompson Aug 2, 2026
6679cc3
Potential fix for pull request finding
tonythethompson Aug 2, 2026
01c0e44
Update src/cmd/setup.rs
tonythethompson Aug 2, 2026
da3daec
Update src/nu/bootstrap.rs
tonythethompson Aug 2, 2026
2635dbd
Update src/state/migration_journal.rs
tonythethompson Aug 2, 2026
d52cda6
Update src/nu/version_manager.rs
tonythethompson Aug 2, 2026
d0a5c27
Update src/nu/migrate_legacy.rs
tonythethompson Aug 2, 2026
db0cbb0
Merge branch 'master' into pr-migrate-legacy-installs
tonythethompson Aug 2, 2026
a867a90
fix(migrate): address PR 69 unresolved review threads (8 fixes)
tonythethompson Aug 2, 2026
5bbdaf5
fix(migrate): rebase fallout — close stray fn, drop unused imports, g…
tonythethompson Aug 2, 2026
0e8863e
docs(agents): list nu/migrate_legacy.rs in the project structure
tonythethompson Aug 2, 2026
323fcb3
fix(migrate): address PR 69 remaining review threads (7 fixes)
tonythethompson Aug 2, 2026
5705910
refactor(setup): route setup_subcommand_lock across destructive setup…
tonythethompson Aug 2, 2026
e14959f
refactor(setup): require_tty_or_yes seam + apply across all destructi…
tonythethompson Aug 2, 2026
44381ff
feat(setup): --force flag for `setup nu use <path>` opt-in to destruc…
tonythethompson Aug 2, 2026
81ac8ee
fix: 4 findings — Lock direct setup execution; Fix setup test arguments;
qodo-code-review[bot] Aug 2, 2026
471347c
fix: Include legacy installs in version listing
qodo-code-review[bot] Aug 2, 2026
cea6efd
fix(migrate): install into versioned layout (PR69 Srm) + versioned-la…
tonythethompson Aug 2, 2026
ce81a65
audit(destructive): require_tty_or_yes for remove and snapshot delete…
tonythethompson Aug 2, 2026
2682b97
Update migrate_legacy.rs
tonythethompson Aug 2, 2026
225dc72
Merge origin/master into feature/setup-subcommand-lock
cursoragent Aug 2, 2026
bfa9ac3
Fix PR #71 review: version-before-mutation and force gates
cursoragent Aug 2, 2026
302d5b6
Fix PR #71 review: TTY seams, migration safety, marker order
tonythethompson Aug 3, 2026
8b8b1cd
Fix rustfmt in migrate_legacy regression test
tonythethompson Aug 3, 2026
3b655fb
fix(nu): return VersionManagerError from public version APIs
cursoragent Aug 3, 2026
8f009ff
Fix PR #71 review: docs, doctor hints, detect timeout
tonythethompson Aug 3, 2026
85dcc8c
Fix PR #71 review: PATH test mutex and clear-before-delete
tonythethompson Aug 3, 2026
438f695
fix(pr71): hoist migration symlink guard and cover use snapshots
cursoragent Aug 3, 2026
e34b055
fix: Validate migration journal versions on load
qodo-code-review[bot] Aug 3, 2026
9164e3d
fix: Validate preserved off-tree binaries
qodo-code-review[bot] Aug 3, 2026
98d0649
Potential fix for pull request finding
tonythethompson Aug 3, 2026
9d80322
Potential fix for pull request finding
tonythethompson Aug 3, 2026
cbdf1e4
Update src/state/migration_journal.rs
tonythethompson Aug 3, 2026
9a25b74
Update src/cmd/remove.rs
tonythethompson Aug 3, 2026
58b8521
fix: apply PR83 review fixes across bootstrap, doctor, migrate_legacy…
cursoragent Aug 3, 2026
6175a61
fix: finish remaining PR83 review items
cursoragent Aug 3, 2026
7f58734
style: rustfmt remove confirm message formatting
cursoragent Aug 3, 2026
3de0849
fix: close remaining PR83 review gaps
cursoragent Aug 3, 2026
1f9281f
Merge origin/master into cursor/pr83-review-fixes-b0ef
cursoragent Aug 3, 2026
8c36d34
fix: macOS path containment, consent-before-network, empty managed tree
tonythethompson Aug 4, 2026
fc91b2e
fix: do not silently clear unreadable active-version marker on remove
tonythethompson Aug 4, 2026
7017097
Merge branch 'master' into cursor/pr71-review-fixes-a7e4
tonythethompson Aug 4, 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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ src/
nupm.rs — `numan nupm status|inspect|import|diff`: nupm discovery + import + drift (Phase 6.1–6.3)
completions.rs — `numan completions <shell>`: bash/fish/zsh/powershell/nushell scripts (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 [--no-activate]`: curated starter install + activate for current Nu
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
nu_pin_offer.rs — Shared TTY offer to `setup nu <version>` + `init --refresh` on Nu mismatch
install/
Expand Down Expand Up @@ -144,7 +144,7 @@ tests/
- **Activation scope**: `PluginActivation` struct stores `(nu_executable_sha256, nu_version, plugin_registry_path)`; a plugin is "active" only when all three match the current `NuPaths` — bare `bool` would go stale after `numan init --refresh`
- **Journal**: `state/pending-activation.json` written as all-`prepared` before first registration; each entry advances to `registered` atomically before lockfile update; reconciled on next `activate` run if process is interrupted
- **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.
- **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).
- **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
102 changes: 99 additions & 3 deletions src/cmd/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,35 @@ fn apply_repairs(

drop(lock.take());

// Reconcile pending migration journals BEFORE off-PATH Nu registration.
// A Prepared orphan empty `<version>/` under tools/nushell makes
// `setup nu use` refuse without `--force`; cleaning it first lets one
// `doctor --fix` pass complete both repairs.
if findings
.iter()
.any(|f| f.id == "journal.migration_pending" && f.severity == Severity::Warn)
{
let id = "journal.migration_repaired".to_string();
let _migration_repair_lock = acquire_mutation_lock(root)?;
match migration_journal::reconcile(root) {
Ok(Some(_)) => records.push(RepairRecord {
id,
status: RepairStatus::Applied,
reason: None,
}),
Ok(None) => records.push(RepairRecord {
id,
status: RepairStatus::Skipped,
reason: None,
}),
Err(e) => records.push(RepairRecord {
id,
status: RepairStatus::Failed,
reason: Some(e.to_string()),
}),
}
}

if findings
.iter()
.any(|f| f.id == "nu.binary.found_off_path" && f.severity == Severity::Warn)
Expand Down Expand Up @@ -2386,14 +2415,36 @@ mod tests {
.save(root)
.unwrap();

let args = DoctorArgs {
// Capture the findings first so we can call apply_repairs directly and
// inspect the returned RepairRecord list.
let scan_args = DoctorArgs {
scan: true,
json: false,
nupm_home: None,
};
let report = run_checks_with_options(&scan_args, root, &test_doctor_options()).unwrap();

let fix_args = DoctorArgs {
scan: false,
json: false,
nupm_home: None,
};
let _ = execute_with_options(&args, root, test_doctor_options()).unwrap();
let records =
apply_repairs(&fix_args, root, &report.findings, &test_doctor_options()).unwrap();

// The repair record for the migration journal must be Applied.
let rec = records
.iter()
.find(|r| r.id == "journal.migration_repaired")
.expect("journal.migration_repaired repair record must be present");
assert_eq!(
rec.status,
RepairStatus::Applied,
"migration journal repair must be Applied, got: {:?}",
rec.status
);

// After `doctor --fix`: the empty subdir AND the journal must be gone.
// After repair: the empty subdir AND the journal must be gone.
assert!(
!tools.join("0.113.1").exists(),
"empty versioned subdir must be removed by reconcile"
Expand All @@ -2404,6 +2455,51 @@ mod tests {
);
}

/// A well-formed journal with an unknown `schema_version` must surface a
/// `journal.migration_invalid` finding (Error severity, Manual repair tier)
/// and must NOT produce a `journal.migration_pending` finding.
#[test]
fn doctor_reports_unsupported_schema_version_as_invalid() {
let dir = TempDir::new().unwrap();
let root = dir.path();

// Construct a well-formed JSON journal with an unsupported schema_version.
let journal_path = PendingMigration::journal_path(root);
std::fs::create_dir_all(journal_path.parent().unwrap()).unwrap();
let content = serde_json::json!({
"schema_version": 9999,
"version": "0.113.1",
"stage": "prepared"
});
std::fs::write(&journal_path, serde_json::to_vec(&content).unwrap()).unwrap();

let report = run_checks_with_options(
&DoctorArgs {
scan: true,
json: false,
nupm_home: None,
},
root,
&test_doctor_options(),
)
.unwrap();

let invalid = report
.findings
.iter()
.find(|f| f.id == "journal.migration_invalid")
.expect("journal.migration_invalid must be reported for unknown schema_version");
assert_eq!(invalid.severity, Severity::Error);
assert_eq!(invalid.repair, RepairTier::Manual);
assert!(
report
.findings
.iter()
.all(|f| f.id != "journal.migration_pending"),
"migration_pending must NOT be reported alongside migration_invalid"
);
}

/// PR69 WCk regression: a malformed `migration-journal.json` must
/// surface a `journal.migration_invalid` finding at Error severity with
/// a Manual repair tier. Previously the report silently dropped the
Expand Down
8 changes: 8 additions & 0 deletions src/cmd/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ fn execute_with_tty(args: &RemoveArgs, root: &Path, is_tty: bool) -> Result<()>
// Refuse unattended (non-TTY) sessions without explicit --yes so safe-batch
// automation has to opt in; interactive sessions keep the existing flow.
crate::util::confirm::require_tty_or_yes_with_seam(args.yes, "package removal", is_tty)?;
Comment thread
tonythethompson marked this conversation as resolved.
crate::util::confirm::confirm_or_bail(
&format!(
"Remove package '{}' ? This deletes its payload.",
args.package
),
args.yes,
"Cancelled.",
)?;

let _lock = acquire_mutation_lock(root)?;

Expand Down
Loading
Loading