Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
25 changes: 15 additions & 10 deletions src/cmd/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ pub fn format_info(pkg: &Package, platform: &Platform, nu: Option<&NuVersion>) -
let mut out = String::new();
out.push_str(&format!("Package: {}/{}\n", pkg.id.owner, pkg.id.name));
out.push_str(&format!("Type: {}\n", pkg.package_type));
match pkg.package_type {
crate::core::package::PackageType::Script
| crate::core::package::PackageType::Completion => {
out.push_str(
"Activation: install-only (activation deferred; install does not wire Nu)\n",
);
}
_ => {}
}
out.push_str("Status: verified upstream artifact\n");
out.push_str(&format!("Description: {}\n", pkg.description));
out.push_str(&format!("Repository: {}\n", pkg.repo));
Expand Down Expand Up @@ -157,16 +166,12 @@ mod tests {
}

#[test]
fn format_info_includes_verified_status_and_disclaimer() {
let pkg = sample_plugin(false);
let nu = NuVersion::parse("0.113.1").unwrap();
let out = format_info(&pkg, &linux_platform(), Some(&nu));
assert!(
out.contains("Status: verified upstream artifact"),
"{out}"
);
assert!(out.contains("has not security-audited"), "{out}");
assert!(!out.to_lowercase().contains("approved"), "{out}");
fn format_info_marks_script_install_only() {
let mut pkg = sample_plugin(false);
pkg.package_type = PackageType::Script;
let out = format_info(&pkg, &linux_platform(), None);
assert!(out.contains("Activation: install-only"), "{out}");
assert!(out.contains("Type: script"), "{out}");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

#[test]
Expand Down
22 changes: 17 additions & 5 deletions src/cmd/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ fn format_search_header(nu: Option<&NuVersion>, triple: &str) -> String {

/// Row status suffix (leading space + brackets), or empty.
///
/// Plugins get a hard evaluated verdict. Non-plugins never use `[compatible]`;
/// they use not-ABI-locked wording and surface `verified_with` when present.
/// Plugins get a hard evaluated verdict. Modules use not-ABI-locked wording.
/// Scripts and completions are install-only until activation contracts land.
fn format_row_status(
pkg_type: &PackageType,
compatible: bool,
Expand All @@ -159,7 +159,17 @@ fn format_row_status(
String::new()
}
}
_ => {
PackageType::Script | PackageType::Completion => {
Comment thread
tonythethompson marked this conversation as resolved.
if verified_with.is_empty() {
" [install-only; activation deferred]".to_string()
} else {
format!(
" [install-only; activation deferred; verified with {}]",
verified_with.join(", ")
)
}
}
PackageType::Module => {
if verified_with.is_empty() {
" [not ABI-locked]".to_string()
} else {
Expand Down Expand Up @@ -297,11 +307,13 @@ mod tests {
}

#[test]
fn script_and_completion_use_module_style_labels() {
fn script_and_completion_use_install_only_labels() {
for ty in [PackageType::Script, PackageType::Completion] {
let status = format_row_status(&ty, true, true, None, &["0.113.1".to_string()]);
assert!(status.contains("not ABI-locked"), "{ty}");
assert!(status.contains("install-only"), "{ty}");
assert!(status.contains("activation deferred"), "{ty}");
assert!(!status.contains("[compatible]"), "{ty}");
assert!(!status.contains("not ABI-locked"), "{ty}");
}
}

Expand Down
162 changes: 139 additions & 23 deletions src/cmd/try_cmd.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
use anyhow::{bail, Context, Result};
use clap::Parser;
use std::path::Path;

use crate::cmd::activate::{self, ActivateArgs};
use crate::cmd::nu_pin_offer;
use crate::core::nu_version::NuVersion;
Expand All @@ -10,8 +6,12 @@ use crate::core::platform::{Os, Platform};
use crate::core::registry::RegistryManager;
use crate::core::resolve::Resolver;
use crate::install::transaction;
use crate::state::lockfile::Lockfile;
use crate::util::fs_safety::acquire_mutation_lock;
use crate::util::hints::{self, CMD_REGISTRY_SYNC};
use anyhow::{bail, Context, Result};
use clap::Parser;
use std::path::{Path, PathBuf};

/// Install and activate a starter package that fits your current Nu.
///
Expand Down Expand Up @@ -56,6 +56,17 @@ const STARTERS: &[StarterSpec] = &[
nu_minor: None,
os: None,
},
// Nu-agnostic install-only fallbacks (prefer activatable starters above).
StarterSpec {
id: "SuaveIV/nu_script_wttr",
nu_minor: None,
os: None,
},
StarterSpec {
id: "Sanceilaks/nufetch",
nu_minor: None,
os: None,
},
];

pub fn execute(args: &TryArgs, root: &Path) -> Result<()> {
Expand Down Expand Up @@ -144,8 +155,24 @@ pub fn execute(args: &TryArgs, root: &Path) -> Result<()> {
transaction::install_package(&package_id, None, &options)?;
}

if args.no_activate {
println!("Installed '{package_id}' (not activated). Run `numan activate {package_id}`.");
let selected = packages.iter().find(|p| p.id.to_string() == package_id);
let install_only = selected
.map(|p| {
matches!(
p.package_type,
PackageType::Script | PackageType::Completion
)
})
.unwrap_or(false);

if args.no_activate || install_only {
if install_only {
print_install_only_hint(root, &package_id, selected);
} else {
println!(
"Installed '{package_id}' (not activated). Run `numan activate {package_id}`."
);
}
return Ok(());
}

Expand Down Expand Up @@ -177,13 +204,17 @@ enum StarterSelection {
},
}

fn package_is_activatable(pkg: &Package) -> bool {
matches!(pkg.package_type, PackageType::Plugin | PackageType::Module)
}

fn select_starter(
packages: &[Package],
resolver: &Resolver<'_>,
platform: &Platform,
nu: &NuVersion,
) -> StarterSelection {
// 1. Curated starters that match OS + Nu minor and are compatible.
// 1. Curated activatable starters that match OS + Nu minor and are compatible.
for spec in STARTERS {
if let Some(os) = spec.os {
if platform.os != os {
Expand All @@ -196,27 +227,41 @@ fn select_starter(
}
}
if let Some(pkg) = packages.iter().find(|p| p.id.to_string() == spec.id) {
if resolver.has_compatible_version(pkg) {
if package_is_activatable(pkg) && resolver.has_compatible_version(pkg) {
return StarterSelection::Compatible(spec.id.to_string());
}
}
}

// 2. Any curated activatable starter that is compatible regardless of Nu minor.
for spec in STARTERS {
if let Some(os) = spec.os {
if platform.os != os {
continue;
}
}
if let Some(pkg) = packages.iter().find(|p| p.id.to_string() == spec.id) {
if package_is_activatable(pkg) && resolver.has_compatible_version(pkg) {
return StarterSelection::Compatible(spec.id.to_string());
}
}
}

// 2. Any curated starter that is compatible regardless of Nu minor table miss.
// 3. Nu-agnostic install-only script/completion starters (never pin-offer).
for spec in STARTERS {
if let Some(os) = spec.os {
if platform.os != os {
continue;
}
}
if let Some(pkg) = packages.iter().find(|p| p.id.to_string() == spec.id) {
if resolver.has_compatible_version(pkg) {
if !package_is_activatable(pkg) && resolver.has_compatible_version(pkg) {
return StarterSelection::Compatible(spec.id.to_string());
}
}
}

// 3. Curated starter with a suggested Nu pin (prefer skim / Windows semver / nutest).
// 4. Curated activatable starter with a suggested Nu pin.
let mut suggested_pin = None;
for spec in STARTERS {
if let Some(os) = spec.os {
Expand All @@ -225,14 +270,16 @@ fn select_starter(
}
}
if let Some(pkg) = packages.iter().find(|p| p.id.to_string() == spec.id) {
if !package_is_activatable(pkg) {
continue;
}
let diagnosis = resolver.diagnose_package(pkg);
if nu_pin_offer::is_nu_mismatch(&diagnosis) && diagnosis.suggested_pin.is_some() {
return StarterSelection::NeedsPin {
id: spec.id.to_string(),
diagnosis,
};
}
// Remember first pin discovered for None fallback.
if suggested_pin.is_none() && nu_pin_offer::is_nu_mismatch(&diagnosis) {
suggested_pin = diagnosis.suggested_pin;
}
Expand Down Expand Up @@ -282,6 +329,29 @@ fn print_usage_hint(package_id: &str, packages: &[Package]) {
}
}

fn print_install_only_hint(root: &Path, package_id: &str, pkg: Option<&Package>) {
println!("Installed '{package_id}' (install-only; activation deferred).");
let entry = pkg
.and_then(|p| p.versions.last())
.and_then(|v| v.artifact.entry.as_deref());
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
Comment thread
tonythethompson marked this conversation as resolved.
Outdated
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated
let payload = Lockfile::load(root)
.ok()
.and_then(|lf| lf.packages.get(package_id).map(|e| e.payload_path.clone()));
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated
match (payload, entry) {
(Some(rel), Some(entry_name)) => {
let full: PathBuf = root.join(rel).join(entry_name);
println!("In Nu: overlay use {}", full.display());
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
tonythethompson marked this conversation as resolved.
Outdated
}
(Some(rel), None) => {
let full: PathBuf = root.join(rel);
println!("Installed under {}", full.display());
}
_ => {
println!("Use `numan list` to find the installed payload path.");
}
}
}

fn detect_nu(root: &Path) -> Result<NuVersion> {
NuVersion::from_paths_or_detect(root)
.context("Could not detect Nu version. Run `numan init` first.")
Expand Down Expand Up @@ -310,6 +380,18 @@ mod tests {
}

fn pkg(id: &str, constraint: &str, plugin: bool) -> Package {
pkg_typed(
id,
constraint,
if plugin {
PackageType::Plugin
} else {
PackageType::Module
},
)
}

fn pkg_typed(id: &str, constraint: &str, package_type: PackageType) -> Package {
let (owner, name) = id.split_once('/').unwrap();
let mut targets = HashMap::new();
targets.insert(
Expand All @@ -328,39 +410,40 @@ mod tests {
executable_path: "p".to_string(),
},
);
let is_plugin = package_type == PackageType::Plugin;
Package {
id: ScopedId::new(owner, name),
description: "d".to_string(),
repo: "https://example.com".to_string(),
package_type: if plugin {
PackageType::Plugin
} else {
PackageType::Module
},
package_type,
tags: vec![],
versions: vec![VersionEntry {
version: semver::Version::new(1, 0, 0),
nu_version: constraint.to_string(),
verified_with: vec!["0.113.1".to_string()],
artifact: Artifact {
kind: if plugin {
kind: if is_plugin {
"binary".to_string()
} else {
"archive".to_string()
},
url: if plugin {
url: if is_plugin {
None
} else {
Some("https://example.com/m.zip".to_string())
},
sha256: if plugin { None } else { Some("cc".to_string()) },
targets: if plugin { targets } else { HashMap::new() },
sha256: if is_plugin {
None
} else {
Some("cc".to_string())
},
targets: if is_plugin { targets } else { HashMap::new() },
archive_root: None,
include: None,
entry: if plugin {
entry: if is_plugin {
None
} else {
Some("mod.nu".to_string())
Some("entry.nu".to_string())
},
},
source: None,
Expand Down Expand Up @@ -388,6 +471,8 @@ mod tests {
pkg("idanarye/nu_plugin_skim", ">=0.114.0 <0.115.0", true),
pkg("vyadh/nutest", ">=0.103.0", false),
pkg("abusch/nu_plugin_semver", ">=0.113.0 <0.114.0", true),
pkg_typed("SuaveIV/nu_script_wttr", "*", PackageType::Script),
pkg_typed("Sanceilaks/nufetch", "*", PackageType::Script),
];
match select_starter(&packages, &resolver, &platform, &nu) {
StarterSelection::Compatible(id) => assert_eq!(id, "idanarye/nu_plugin_skim"),
Expand Down Expand Up @@ -425,6 +510,37 @@ mod tests {
}
}

#[test]
fn select_starter_falls_back_to_script_before_pin_offer() {
let platform = windows_platform();
let nu = NuVersion::parse("0.114.1").unwrap();
let resolver = Resolver::new(&platform, &nu);
let packages = vec![
pkg("abusch/nu_plugin_semver", ">=0.113.0 <0.114.0", true),
pkg_typed("SuaveIV/nu_script_wttr", "*", PackageType::Script),
pkg_typed("Sanceilaks/nufetch", "*", PackageType::Script),
];
match select_starter(&packages, &resolver, &platform, &nu) {
StarterSelection::Compatible(id) => assert_eq!(id, "SuaveIV/nu_script_wttr"),
other => panic!("unexpected selection: {other:?}"),
}
}

#[test]
fn select_starter_prefers_wttr_when_only_scripts_present() {
let platform = windows_platform();
let nu = NuVersion::parse("0.115.0").unwrap();
let resolver = Resolver::new(&platform, &nu);
let packages = vec![
pkg_typed("Sanceilaks/nufetch", "*", PackageType::Script),
pkg_typed("SuaveIV/nu_script_wttr", "*", PackageType::Script),
];
match select_starter(&packages, &resolver, &platform, &nu) {
StarterSelection::Compatible(id) => assert_eq!(id, "SuaveIV/nu_script_wttr"),
other => panic!("unexpected selection: {other:?}"),
}
}

#[test]
fn format_no_compatible_starter_with_pin_is_honest() {
let msg =
Expand Down
Loading