Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
52ad910
refactor: UX improvements — reduce prompts and simplify doctor command
tonythethompson Aug 1, 2026
5dc3e08
fix: 2 findings — Remove stale DeactivateArgs yes fields; Remove stale A
qodo-code-review[bot] Aug 1, 2026
b95ab9f
fix: Remove obsolete activate yes hints
qodo-code-review[bot] Aug 1, 2026
a6c9fd8
fix: Update doctor documentation and hints
qodo-code-review[bot] Aug 1, 2026
3bc34fd
fix: 2 findings — Remove redundant prompting comments; Clarify concurren
qodo-code-review[bot] Aug 1, 2026
f8bea34
refactor: update command descriptions to reflect plugins+modules support
tonythethompson Aug 1, 2026
180eac8
fix: remove stale yes field from activate_test
tonythethompson Aug 1, 2026
f04c4e7
style: apply formatting
tonythethompson Aug 1, 2026
5c83980
fix: address PR review — remove stale --yes from test support, fix no…
tonythethompson Aug 1, 2026
59f4f92
Merge branch 'master' into refactor/ux-improvements
tonythethompson Aug 2, 2026
0f775bc
Merge branch 'master' into refactor/ux-improvements
tonythethompson Aug 2, 2026
15e6848
refactor: UX improvements — reduce prompts and simplify doctor command
tonythethompson Aug 1, 2026
09cf2a0
fix: 2 findings — Remove stale DeactivateArgs yes fields; Remove stale A
qodo-code-review[bot] Aug 1, 2026
e7cfa16
fix: Remove obsolete activate yes hints
qodo-code-review[bot] Aug 1, 2026
89f3b28
fix: Update doctor documentation and hints
qodo-code-review[bot] Aug 1, 2026
ae50dd1
fix: 2 findings — Remove redundant prompting comments; Clarify concurren
qodo-code-review[bot] Aug 1, 2026
f8716d0
refactor: update command descriptions to reflect plugins+modules support
tonythethompson Aug 1, 2026
a4b4930
fix: remove stale yes field from activate_test
tonythethompson Aug 1, 2026
8623a74
style: apply formatting
tonythethompson Aug 1, 2026
37e3884
fix: address PR review — remove stale --yes from test support, fix no…
tonythethompson Aug 1, 2026
c26f509
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] Aug 2, 2026
e648e7f
fix: unbreak doctor tests after TTY confirm gating
cursoragent Aug 2, 2026
6af9a80
fix: address Copilot PR 68 review findings
cursoragent Aug 2, 2026
6d231f2
fix: keep doctor --json stdout as a single JSON object
cursoragent Aug 3, 2026
708b24e
Merge branch 'refactor/ux-improvements' of https://github.com/tonythe…
tonythethompson Aug 3, 2026
c5ef084
fix: import IsTerminal and rustfmt TryArgs
cursoragent Aug 3, 2026
2d81487
fix: address CodeRabbit doctor UX and test feedback
cursoragent Aug 3, 2026
32f4944
fix: harden doctor off-path skip test and snapshot refresh/repairs
cursoragent Aug 3, 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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ src/
registry.rs — Registry management subcommands
activate.rs — Plugin + module activation (Phase 3 & 4); public entry: execute_with_candidate_runner
init.rs — `numan init [--refresh]`: Nu probe, paths cache, auto-configures official registry
doctor.rs — `numan doctor [--fix] [--yes]`: health checks + safe repairs (Phase 7.2; spec: docs/numan-doctor.md)
doctor.rs — `numan doctor [--scan]`: health checks + safe repairs (or report-only scan) (Phase 7.2; spec: docs/numan-doctor.md)
snapshot.rs — `numan snapshot list|inspect|delete|rollback` (Phase 5.3)
deactivate.rs — Plugin + module deactivation: journaled plugin unregister (`execute_with_unregistrar`); module full/partial (Phase 4 / Issue #22 PR2)
plugin_lifecycle.rs — Activate/deactivate-owned lifecycle boundary exposed to opt-in update orchestration (Issue #22 PR3)
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,8 @@ See [docs/snapshots-and-rollback.md](docs/snapshots-and-rollback.md) for scope,
#### 6. Verify health

```bash
numan doctor # report-only diagnosis
numan doctor --fix --yes # apply safe automated repairs
numan doctor # diagnose and apply safe automated repairs
numan doctor --scan # report-only diagnosis
```

---
Expand Down Expand Up @@ -333,7 +333,7 @@ Global flag: `--root <path>` — override the Numan root directory (all commands
| `numan nupm import --manifest file.toml` | Batch import from manifest |
| `numan nupm diff <owner/name>` | Compare imported payload vs nupm source |
| `numan completions <shell>` | Generate bash, fish, zsh, powershell, or nushell completions |
| `numan doctor [--fix] [--yes] [--json]` | Diagnose root health; optional safe repairs |
| `numan doctor [--scan] [--json]` | Diagnose root health; repairs by default |

### Common flags (by command)

Expand Down
10 changes: 5 additions & 5 deletions docs/numan-doctor.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ It answers: *“Is this Numan root consistent, safe to mutate, and aligned with
## Invocation

```text
numan doctor [--fix] [--yes] [--json] [--nupm-home PATH]
numan doctor [--scan] [--json] [--nupm-home PATH]
```

| Flag | Behavior |
|------|----------|
| `--fix` | After reporting, apply automated repairs (see [Repair policy](#repair-policy)) |
| `--yes` | Skip confirmation prompts for **confirm**-tier repairs (non-TTY implies `--yes` for confirm tier only) |
| `--scan` | Report findings without applying repairs |

| `--json` | Emit a single JSON object (schema versioned); no ANSI styling. With `--fix`, include `repairs` attempted/applied |
| `--nupm-home PATH` | Override nupm home for the optional coexistence section (same resolution order as `numan nupm status`) |

Global `--root` applies as for all commands.

**Default (no flags):** diagnose and print findings + manual fix hints.
**`--fix`:** diagnose, then repair what is allowed without user-supplied data.
**Default (no flags):** diagnose and apply available repairs, then print findings.
**`--scan`:** diagnose and print findings without mutating state.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Exit codes

Expand Down
6 changes: 3 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ pub enum Commands {
Remove(cmd::remove::RemoveArgs),
/// Garbage-collect orphaned package directories
Gc(cmd::gc::GcArgs),
/// Activate installed plugins with Nu
/// Activate installed plugins and modules with Nu
Activate(cmd::activate::ActivateArgs),
/// Deactivate active modules
/// Deactivate active plugins and modules
Deactivate(cmd::deactivate::DeactivateArgs),
/// List all installed packages
List,
Expand All @@ -54,7 +54,7 @@ pub enum Commands {
Nupm(cmd::nupm::NupmArgs),
/// Generate shell completion scripts
Completions(cmd::completions::CompletionsArgs),
/// Diagnose Numan root health and optionally apply safe repairs
/// Diagnose Numan root health and apply safe repairs
Doctor(cmd::doctor::DoctorArgs),
/// Install optional Nushell integration helpers
#[command(subcommand)]
Expand Down
18 changes: 6 additions & 12 deletions src/cmd/activate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,12 @@ pub struct ActivateArgs {
/// Package IDs (owner/name) to activate. Omit to activate all installed inactive packages.
pub packages: Vec<String>,

/// Skip confirmation prompts
#[arg(long)]
pub yes: bool,
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

/// Show detailed output
#[arg(long)]
pub verbose: bool,

/// List all installed packages and their activation status (read-only)
#[arg(long, conflicts_with_all = ["yes", "packages"])]
#[arg(long, conflicts_with = "packages")]
Comment thread
tonythethompson marked this conversation as resolved.
pub list: bool,

/// Check activation integrity for packages (read-only, no mutation)
Expand Down Expand Up @@ -154,16 +150,14 @@ fn execute_with_registrar_and_runner(
};
drop(planning_lock);

// 8. Consent table + confirmation
// 8. Consent table (informational only; no prompt)
print_grouped_consent_table(
&plugin_targets,
&module_targets,
managed_file_path.as_deref(),
&nu_paths.plugin_registry_path,
);

crate::util::confirm::confirm_or_bail("Proceed?", args.yes, "Activation cancelled.")?;

// 9. Reacquire the root mutation lock after consent.
let _lock = acquire_mutation_lock(root)?;

Expand Down Expand Up @@ -1421,7 +1415,7 @@ mod tests {

let args = ActivateArgs {
packages: vec!["owner/myscript".to_string()],
yes: true,

verbose: false,
list: false,
check: false,
Expand Down Expand Up @@ -1501,7 +1495,7 @@ mod tests {

let args = ActivateArgs {
packages: vec!["owner/mycomp".to_string()],
yes: true,

verbose: false,
list: false,
check: false,
Expand Down Expand Up @@ -1587,7 +1581,7 @@ mod tests {

let args = ActivateArgs {
packages: vec!["owner/mymod".to_string()],
yes: true,

verbose: false,
list: false,
check: false,
Expand Down Expand Up @@ -1669,7 +1663,7 @@ mod tests {

let args = ActivateArgs {
packages: vec!["owner/depmod".to_string()],
yes: true,

verbose: false,
list: false,
check: false,
Expand Down
54 changes: 24 additions & 30 deletions src/cmd/deactivate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ pub struct DeactivateArgs {
/// Package IDs (owner/name) to deactivate. Omit to deactivate all active plugins and modules.
pub packages: Vec<String>,

/// Skip confirmation prompts
#[arg(long)]
pub yes: bool,
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

/// Show detailed output
#[arg(long)]
pub verbose: bool,
Expand Down Expand Up @@ -146,11 +142,9 @@ pub fn execute_with_candidate_runner_and_unregistrar(
}
drop(planning_lock);

// 5. Show consent table and confirm
// 5. Show consent table (informational only; no prompt)
print_consent_table(&targets_requested, &nu_paths.plugin_registry_path);

crate::util::confirm::confirm_or_bail("Proceed?", args.yes, "Deactivation cancelled.")?;

// 6. Reacquire the root mutation lock after consent.
let _lock = acquire_mutation_lock(root)?;

Expand All @@ -166,7 +160,7 @@ pub fn execute_with_candidate_runner_and_unregistrar(
// on the current authoritative state.
let mut lockfile = Lockfile::load(root)?;
let targets_requested =
reclassify_confirmed_targets(args, &lockfile, root, &nu_paths, &targets_requested)?;
reclassify_targets(args, &lockfile, root, &nu_paths, &targets_requested)?;
if targets_requested.is_empty() {
println!("Nothing to deactivate.");
return Ok(());
Expand Down Expand Up @@ -505,17 +499,17 @@ fn active_plugin_from_entry(
})
}

fn reclassify_confirmed_targets(
fn reclassify_targets(
args: &DeactivateArgs,
lockfile: &Lockfile,
root: &Path,
nu_paths: &NuPaths,
confirmed_targets: &ClassifiedTargets,
planned_targets: &ClassifiedTargets,
) -> Result<ClassifiedTargets> {
let current_targets = classify_and_validate_packages(args, lockfile, root, nu_paths)?;
if current_targets != *confirmed_targets {
if current_targets != *planned_targets {
bail!(
"Activation state changed after confirmation. No packages were deactivated; retry the command to review the current targets."
"Activation state changed since planning; no packages were deactivated. Retry to review current targets."
);
}
Ok(current_targets)
Expand Down Expand Up @@ -1339,7 +1333,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec!["owner/myplugin".to_string()],
yes: true,

verbose: false,
};

Expand All @@ -1360,7 +1354,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec!["owner/myplugin".to_string()],
yes: true,

verbose: false,
};

Expand All @@ -1381,7 +1375,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec![],
yes: true,

verbose: false,
};

Expand All @@ -1397,7 +1391,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec!["owner/myplugin".to_string()],
yes: true,

verbose: false,
};

Expand All @@ -1414,7 +1408,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec!["owner/myscript".to_string()],
yes: true,

verbose: false,
};

Expand All @@ -1433,7 +1427,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec!["owner/mycomp".to_string()],
yes: true,

verbose: false,
};

Expand All @@ -1453,7 +1447,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec!["owner/mymod".to_string()],
yes: true,

verbose: false,
};

Expand All @@ -1472,7 +1466,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec!["owner/nosuchpkg".to_string()],
yes: true,

verbose: false,
};

Expand All @@ -1492,7 +1486,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec!["owner/mymod".to_string()],
yes: true,

verbose: false,
};

Expand Down Expand Up @@ -1552,7 +1546,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec![],
yes: true,

verbose: false,
};

Expand All @@ -1564,11 +1558,11 @@ mod tests {
}

#[test]
fn reclassification_rejects_expanded_implicit_targets_after_confirmation() {
fn reclassification_rejects_expanded_implicit_targets_after_planning() {
let dir = TempDir::new().unwrap();
let args = DeactivateArgs {
packages: vec![],
yes: true,

verbose: false,
};
let confirmed_lockfile = make_lockfile_with_modules(vec![("owner/alpha", "module", true)]);
Expand All @@ -1584,15 +1578,15 @@ mod tests {
("owner/beta", "module", true),
]);

let error = reclassify_confirmed_targets(
let error = reclassify_targets(
&args,
&changed_lockfile,
dir.path(),
&fake_nu_paths(),
&confirmed_targets,
)
.unwrap_err();
assert!(error.to_string().contains("changed after confirmation"));
assert!(error.to_string().contains("changed since planning"));
}

#[test]
Expand All @@ -1614,7 +1608,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec!["owner/highlight".to_string()],
yes: true,

verbose: false,
};
execute_with_unregistrar(&args, env.root(), &|_nu, identity, _cfg| {
Expand Down Expand Up @@ -1671,7 +1665,7 @@ mod tests {
let called = AtomicBool::new(false);
let args = DeactivateArgs {
packages: vec![],
yes: true,

verbose: false,
};
let err = execute_with_unregistrar(&args, env.root(), &|_nu, _identity, _cfg| {
Expand Down Expand Up @@ -1731,7 +1725,7 @@ mod tests {
let called = AtomicBool::new(false);
let args = DeactivateArgs {
packages: vec!["owner/highlight".to_string()],
yes: true,

verbose: false,
};
execute_with_unregistrar(&args, env.root(), &|_nu, _identity, _cfg| {
Expand Down Expand Up @@ -1762,7 +1756,7 @@ mod tests {

let args = DeactivateArgs {
packages: vec!["owner/highlight".to_string()],
yes: true,

verbose: false,
};
let err = execute_with_unregistrar(&args, env.root(), &|_nu, _name, _cfg| {
Expand Down
Loading
Loading