Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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: 1 addition & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ pub enum Commands {
Setup(cmd::setup::SetupCommands),
/// Try a package against your current Nu and platform, with compatibility guidance
Try(cmd::try_cmd::TryArgs),
/// Switch the active managed Nu version (reserved, post-1.0)
/// Switch the active managed Nu version (`latest`, `list`, or a specific version)
Use(cmd::use_cmd::UseArgs),
}
50 changes: 45 additions & 5 deletions src/cmd/completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub fn execute(args: &CompletionsArgs) -> Result<()> {
shell_label(args.shell),
path.display()
);
println!("Re-run after upgrading numan to refresh tab completion.");
if matches!(args.shell, CompletionShell::PowerShell) {
println!(
"Add to $PROFILE (once): . {}",
Expand Down Expand Up @@ -186,21 +187,35 @@ numan completions powershell --print | Add-Content -Encoding utf8 $PROFILE
.to_string(),
CompletionShell::Nushell => "\
# Prefer: numan completions nushell
# Or manually:
mkdir --all ($nu.data-dir | path join vendor/autoload)
# Or manually (mkdir creates parent dirs by default):
mkdir ($nu.data-dir | path join vendor/autoload)
numan completions nushell --print | save -f ($nu.data-dir | path join vendor/autoload/numan-completions.nu)
"
.to_string(),
}
}

/// Build the clap command tree used only for shell completion generation.
///
/// Help subcommands are disabled recursively. `clap_complete` would otherwise
/// emit duplicate `numan help <cmd>` entries (often empty) that roughly double
/// script size without improving tab completion.
fn command_for_completions() -> clap::Command {
disable_help_subcommands_recursively(Cli::command())
}

fn disable_help_subcommands_recursively(cmd: clap::Command) -> clap::Command {
cmd.disable_help_subcommand(true)
.mut_subcommands(disable_help_subcommands_recursively)
}

/// Generate a completion script for `shell`.
///
/// PowerShell output is rewritten so it can be appended to an existing
/// `$PROFILE` that already contains statements (see
/// [`make_powershell_profile_safe`]).
pub fn generate_script(shell: CompletionShell) -> Result<String> {
let mut cmd = Cli::command();
let mut cmd = command_for_completions();
let mut buf = Vec::new();
match shell {
CompletionShell::Bash => generate(Shell::Bash, &mut cmd, "numan", &mut buf),
Expand Down Expand Up @@ -301,10 +316,14 @@ mod tests {
assert!(print_hint(CompletionShell::Zsh).contains("mkdir -p ~/.zfunc"));
assert!(print_hint(CompletionShell::Fish)
.contains("mkdir -p \"${XDG_CONFIG_HOME:-$HOME/.config}/fish/completions\""));
let nu_hint = print_hint(CompletionShell::Nushell);
assert!(nu_hint.contains("vendor/autoload/numan-completions.nu"));
assert!(nu_hint.contains("numan completions nushell --print"));
assert!(
print_hint(CompletionShell::Nushell).contains("vendor/autoload/numan-completions.nu")
!nu_hint.contains("mkdir --all"),
"Nushell mkdir has no --all flag; it creates parents by default"
);
assert!(print_hint(CompletionShell::Nushell).contains("numan completions nushell --print"));
assert!(nu_hint.contains("mkdir ($nu.data-dir | path join vendor/autoload)"));
}

#[test]
Expand Down Expand Up @@ -419,6 +438,27 @@ mod tests {
!script.contains("vendor/autoload"),
"hint must not be in script"
);
assert!(
!script.contains("numan help"),
"help subcommands must not bloat nushell completions"
);
}

#[test]
fn completions_omit_help_subcommands() {
for shell in [
CompletionShell::Bash,
CompletionShell::Fish,
CompletionShell::Zsh,
CompletionShell::PowerShell,
CompletionShell::Nushell,
] {
let script = generate_script(shell).expect("generate");
assert!(
!script.contains("numan help"),
"{shell:?} completions must not include help subcommands"
);
}
}

#[test]
Expand Down
37 changes: 36 additions & 1 deletion src/cmd/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ pub fn format_info(pkg: &Package, platform: &Platform, nu: Option<&NuVersion>) -
}
_ => {}
}
out.push_str("Status: verified upstream artifact\n");
if pkg.id.owner == "numan-maintained" {
out.push_str("Status: numan-maintained fork (not a verified upstream artifact)\n");
Comment on lines +38 to +39

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Authenticate the maintained-fork label

When a custom registry is configured as the default, its index can assign any package the numan-maintained owner, and find_package() returns that package without preserving registry or signer identity. This branch therefore presents a third party's package as a Numan-maintained fork solely from an unreserved string; only emit this ownership claim when the package came from the verified official trust root, or qualify it with the actual registry identity.

AGENTS.md reference: AGENTS.md:L148-L148

Useful? React with 👍 / 👎.

Comment on lines +38 to +39

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 Surface the original upstream for maintained forks

For an actual numan-maintained package, this new output path identifies it as a fork but still prints only pkg.repo and source.git, both of which describe the fork. SourceInfo at src/core/package.rs:146-152 also discards the planned source.upstream field, so numan info cannot tell users which original author or repository was forked, contrary to the maintained-fork inspection contract in docs/plans/2026-08-09-intake-process-reform.md:263-277; deserialize and display that upstream identity with this status.

Useful? React with 👍 / 👎.

} else {
out.push_str("Status: verified upstream artifact\n");
}
out.push_str(&format!("Description: {}\n", pkg.description));
out.push_str(&format!("Repository: {}\n", pkg.repo));
if !pkg.tags.is_empty() {
Expand Down Expand Up @@ -79,6 +83,10 @@ pub fn format_info(pkg: &Package, platform: &Platform, nu: Option<&NuVersion>) -
));
}

if ver.provenance.as_deref() == Some("commit-snapshot") {
out.push_str(" note: built from a commit snapshot, not a tagged release\n");
}

if let Some(ref source) = ver.source {
out.push_str(&format!(" source git: {}\n", source.git));
out.push_str(&format!(" source rev: {}\n", source.rev));
Expand Down Expand Up @@ -161,6 +169,7 @@ mod tests {
}),
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
}],
}
}
Expand Down Expand Up @@ -195,6 +204,32 @@ mod tests {
assert!(out.contains("cargo_name: nu_plugin_highlight"), "{out}");
}

#[test]
fn format_info_notes_commit_snapshot_provenance() {
let mut pkg = sample_plugin(false);
pkg.versions[0].provenance = Some("commit-snapshot".to_string());
let out = format_info(&pkg, &linux_platform(), None);
assert!(
out.contains("built from a commit snapshot, not a tagged release"),
"{out}"
);
}

#[test]
fn format_info_omits_provenance_note_when_absent() {
let pkg = sample_plugin(false);
let out = format_info(&pkg, &linux_platform(), None);
assert!(!out.contains("commit snapshot"), "{out}");
}

#[test]
fn format_info_omits_provenance_note_for_other_values() {
let mut pkg = sample_plugin(false);
pkg.versions[0].provenance = Some("tagged-release".to_string());
let out = format_info(&pkg, &linux_platform(), None);
assert!(!out.contains("commit snapshot"), "{out}");
}

#[test]
fn format_info_omits_source_lines_when_absent() {
let pkg = sample_plugin(false);
Expand Down
1 change: 1 addition & 0 deletions src/cmd/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ mod tests {
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
}],
}
}
Expand Down
1 change: 1 addition & 0 deletions src/cmd/try_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ mod tests {
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
}],
}
}
Expand Down
27 changes: 27 additions & 0 deletions src/core/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ pub struct VersionEntry {
/// `None` for plugins, scripts, and completions.
#[serde(default)]
pub activation: Option<RegistryActivationSpec>,
/// Optional provenance marker, e.g. `"commit-snapshot"` for versions
/// built from a pinned commit with no upstream tag.
#[serde(default)]
pub provenance: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down Expand Up @@ -269,6 +273,29 @@ mod tests {
assert!(entry.source.is_none());
}

#[test]
fn parse_version_entry_defaults_provenance_to_none() {
let json = r#"{
"version": "0.25.2",
"nu_version": ">=0.113.0 <0.114.0",
"artifact": { "kind": "binary", "targets": {} }
}"#;
let entry: VersionEntry = serde_json::from_str(json).unwrap();
assert!(entry.provenance.is_none());
}

#[test]
fn parse_version_entry_with_commit_snapshot_provenance() {
let json = r#"{
"version": "0.0.0-snapshot.20260809.5a1ca2a",
"nu_version": ">=0.114.0 <0.115.0",
"provenance": "commit-snapshot",
"artifact": { "kind": "binary", "targets": {} }
}"#;
let entry: VersionEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.provenance.as_deref(), Some("commit-snapshot"));
}

#[test]
fn parse_version_entry_with_source() {
let json = r#"{
Expand Down
1 change: 1 addition & 0 deletions src/core/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@ mod tests {
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
}],
}],
}
Expand Down
3 changes: 3 additions & 0 deletions src/core/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,7 @@ mod tests {
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
},
VersionEntry {
version: semver::Version::new(1, 0, 0),
Expand All @@ -743,6 +744,7 @@ mod tests {
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
},
],
}
Expand Down Expand Up @@ -1055,6 +1057,7 @@ mod tests {
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
}],
}
}
Expand Down
4 changes: 4 additions & 0 deletions tests/install_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ fn integration_full_install_from_signed_registry() {
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
}],
};

Expand Down Expand Up @@ -381,6 +382,7 @@ fn integration_resolve_exact_rejects_incompatible() {
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
}],
};

Expand Down Expand Up @@ -463,6 +465,7 @@ fn integration_snapshot_before_install() {
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
}],
};

Expand Down Expand Up @@ -533,6 +536,7 @@ fn integration_snapshot_before_install() {
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
}],
};

Expand Down
1 change: 1 addition & 0 deletions tests/support/active_update/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ fn write_dual_version_registry(
source: None,
dependencies: BTreeMap::new(),
activation: None,
provenance: None,
}
};

Expand Down