diff --git a/cli/args/flags.rs b/cli/args/flags.rs index 7b640a446037cf..fe5253fe0bcfb9 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -474,7 +474,7 @@ pub enum DenoSubcommand { #[derive(Clone, Debug, PartialEq, Eq)] pub enum OutdatedKind { - Update { latest: bool }, + Update { latest: bool, interactive: bool }, PrintOutdated { compatible: bool }, } @@ -2653,7 +2653,7 @@ Specific version requirements to update to can be specified: .long("latest") .action(ArgAction::SetTrue) .help( - "Update to the latest version, regardless of semver constraints", + "Consider the latest version, regardless of semver constraints", ) .conflicts_with("compatible"), ) @@ -2662,15 +2662,21 @@ Specific version requirements to update to can be specified: .long("update") .short('u') .action(ArgAction::SetTrue) - .conflicts_with("compatible") .help("Update dependency versions"), ) + .arg( + Arg::new("interactive") + .long("interactive") + .short('i') + .action(ArgAction::SetTrue) + .requires("update") + .help("Interactively select which dependencies to update") + ) .arg( Arg::new("compatible") .long("compatible") .action(ArgAction::SetTrue) - .help("Only output versions that satisfy semver requirements") - .conflicts_with("update"), + .help("Only consider versions that satisfy semver requirements") ) .arg( Arg::new("recursive") @@ -4455,7 +4461,11 @@ fn outdated_parse( let update = matches.get_flag("update"); let kind = if update { let latest = matches.get_flag("latest"); - OutdatedKind::Update { latest } + let interactive = matches.get_flag("interactive"); + OutdatedKind::Update { + latest, + interactive, + } } else { let compatible = matches.get_flag("compatible"); OutdatedKind::PrintOutdated { compatible } diff --git a/cli/tools/registry/pm/outdated.rs b/cli/tools/registry/pm/outdated.rs index 4409d428274992..755a2bd929e20d 100644 --- a/cli/tools/registry/pm/outdated.rs +++ b/cli/tools/registry/pm/outdated.rs @@ -15,6 +15,7 @@ use deno_semver::VersionReq; use deno_terminal::colors; use super::deps::Dep; +use super::deps::DepId; use super::deps::DepManager; use super::deps::DepManagerArgs; use super::deps::PackageLatestVersion; @@ -242,8 +243,11 @@ pub async fn outdated( deps.resolve_versions().await?; match update_flags.kind { - crate::args::OutdatedKind::Update { latest } => { - update(deps, latest, &filter_set, flags).await?; + crate::args::OutdatedKind::Update { + latest, + interactive, + } => { + update(deps, latest, &filter_set, interactive, flags).await?; } crate::args::OutdatedKind::PrintOutdated { compatible } => { print_outdated(&mut deps, compatible)?; @@ -301,9 +305,10 @@ async fn update( mut deps: DepManager, update_to_latest: bool, filter_set: &filter::FilterSet, + interactive: bool, flags: Arc, ) -> Result<(), AnyError> { - let mut updated = Vec::new(); + let mut to_update = Vec::new(); for (dep_id, resolved, latest_versions) in deps .deps_with_resolved_latest_versions() @@ -322,19 +327,66 @@ async fn update( continue; }; - updated.push(( + to_update.push(( dep_id, format!("{}:{}", dep.kind.scheme(), dep.req.name), deps.resolved_version(dep.id).cloned(), new_version_req.clone(), )); + } - deps.update_dep(dep_id, new_version_req); + if interactive { + let selected = interactive::select_interactive( + to_update + .iter() + .map( + |(dep_id, _, current_version, new_req): &( + DepId, + String, + Option, + VersionReq, + )| { + let dep = deps.get_dep(*dep_id); + interactive::PackageInfo { + location: dep.location.clone(), + current_version: current_version + .as_ref() + .map(|nv| nv.version.to_string()) + .unwrap_or_default(), + name: dep + .alias + .as_ref() + .cloned() + .unwrap_or_else(|| dep.req.name.to_string()), + kind: dep.kind, + new_version: new_req + .version_text() + .trim_start_matches('^') + .to_string(), + } + }, + ) + .collect(), + )?; + if let Some(selected) = selected { + let mut i = 0; + to_update.retain(|_| { + let keep = selected.contains(&i); + i += 1; + keep + }); + } else { + log::info!("Cancelled, not updating"); + return Ok(()); + } + } + for (dep_id, _, _, new_version_req) in &to_update { + deps.update_dep(*dep_id, new_version_req.clone()); } deps.commit_changes()?; - if !updated.is_empty() { + if !to_update.is_empty() { let factory = super::npm_install_after_modification( flags.clone(), Some(deps.jsr_fetch_resolver.clone()), @@ -354,7 +406,7 @@ async fn update( let mut deps = deps.reloaded_after_modification(args); deps.resolve_current_versions().await?; for (dep_id, package_name, maybe_current_version, new_version_req) in - updated + to_update { if let Some(nv) = deps.resolved_version(dep_id) { updated_to_versions.insert(( diff --git a/cli/tools/registry/pm/outdated/interactive.rs b/cli/tools/registry/pm/outdated/interactive.rs index 70d677449ea43d..9f27d71d672ac9 100644 --- a/cli/tools/registry/pm/outdated/interactive.rs +++ b/cli/tools/registry/pm/outdated/interactive.rs @@ -1,3 +1,5 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + use std::collections::HashSet; use std::io; use std::io::Write; @@ -12,14 +14,21 @@ use crossterm::style::Stylize; use crossterm::terminal; use crossterm::ExecutableCommand; use crossterm::QueueableCommand; +use deno_core::anyhow; +use deno_core::anyhow::Context; + +use crate::tools::registry::pm::deps::DepKind; use super::super::deps::DepLocation; -use super::OutdatedPackage; #[derive(Debug)] -struct PackageInfo { - location: DepLocation, - package: OutdatedPackage, +pub struct PackageInfo { + pub location: DepLocation, + pub current_version: String, + pub new_version: String, + pub name: String, + + pub kind: DepKind, } #[derive(Debug)] @@ -30,18 +39,19 @@ struct State { name_width: usize, current_width: usize, + start_row: u16, } impl State { - fn new(packages: Vec) -> Self { + fn new(packages: Vec) -> anyhow::Result { let name_width = packages .iter() - .map(|p| p.package.name.len()) + .map(|p| p.name.len() + p.kind.scheme().len() + 1) .max() .unwrap_or_default(); let current_width = packages .iter() - .map(|p| p.package.current.len()) + .map(|p| p.current_version.len()) .max() .unwrap_or_default(); @@ -49,35 +59,36 @@ impl State { packages .sort_by(|a, b| a.location.file_path().cmp(&b.location.file_path())); - Self { + Ok(Self { packages, currently_selected: 0, checked: HashSet::new(), name_width, current_width, - } + start_row: cursor::position()?.1, + }) } - fn render(&self, out: &mut W) -> std::io::Result<()> { + fn render(&self, out: &mut W) -> anyhow::Result<()> { use cursor::MoveTo; use style::Print; use style::PrintStyledContent; crossterm::queue!( out, - terminal::Clear(terminal::ClearType::All), - MoveTo(0, 0), + MoveTo(0, self.start_row), + terminal::Clear(terminal::ClearType::FromCursorDown), PrintStyledContent("?".blue()), )?; - let base = 1; + let base = self.start_row + 1; for (i, package) in self.packages.iter().enumerate() { if self.currently_selected == i { crossterm::queue!( out, - MoveTo(1, base + (self.currently_selected as u16)), + MoveTo(0, base + (self.currently_selected as u16)), PrintStyledContent("❯".blue()), Print(' '), )?; @@ -86,7 +97,7 @@ impl State { let selector = if checked { "●" } else { "○" }; crossterm::queue!( out, - MoveTo(3, base + (i as u16)), + MoveTo(2, base + (i as u16)), Print(selector), Print(" "), )?; @@ -96,14 +107,22 @@ impl State { style::ContentStyle::new().on_black().white().bold(), ))?; } - let want = &package.package.latest; + let want = &package.new_version; + let new_version_highlight = + highlight_new_version(&package.current_version, want)?; + // let style = style::PrintStyledContent() crossterm::queue!( out, Print(format!( - "{: {}", - package.package.name, - package.package.current, - highlight_new_version(&package.package.current, want), + "{: {}", + format_args!( + "{}{}{}", + deno_terminal::colors::gray(package.kind.scheme()), + deno_terminal::colors::gray(":"), + package.name + ), + package.current_version, + new_version_highlight, name_width = self.name_width + 2, current_width = self.current_width )), @@ -136,25 +155,33 @@ struct VersionParts { } impl VersionParts { - fn parse(s: &str) -> VersionParts { + fn parse(s: &str) -> Result { let mut parts = s.splitn(3, '.'); - let major = parts.next().unwrap().parse().unwrap(); - let minor = parts.next().unwrap().parse().unwrap(); - let patch = parts.next().unwrap(); + let major = parts + .next() + .ok_or_else(|| anyhow::anyhow!("expected major version"))? + .parse()?; + let minor = parts + .next() + .ok_or_else(|| anyhow::anyhow!("expected minor version"))? + .parse()?; + let patch = parts + .next() + .ok_or_else(|| anyhow::anyhow!("expected patch version"))?; let (patch, pre) = if patch.contains('-') { let (patch, pre) = patch.split_once('-').unwrap(); (patch, Some(pre.into())) } else { (patch, None) }; - let patch = patch.parse().unwrap(); + let patch = patch.parse()?; let pre = pre.clone(); - Self { + Ok(Self { patch, pre, minor, major, - } + }) } } @@ -168,12 +195,17 @@ fn version_diff(a: &VersionParts, b: &VersionParts) -> VersionDifference { } } -fn highlight_new_version(current: &str, new: &str) -> String { - let current_parts = VersionParts::parse(current); - let new_parts = VersionParts::parse(new); +fn highlight_new_version( + current: &str, + new: &str, +) -> Result { + let current_parts = VersionParts::parse(current) + .with_context(|| format!("parsing current version: {current}"))?; + let new_parts = VersionParts::parse(new) + .with_context(|| format!("parsing new version: {new}"))?; let diff = version_diff(¤t_parts, &new_parts); - match diff { + Ok(match diff { VersionDifference::Major => format!( "{}.{}.{}{}", style::style(new_parts.major).red().bold(), @@ -204,15 +236,51 @@ fn highlight_new_version(current: &str, new: &str) -> String { .map(|pre| pre.green().to_string()) .unwrap_or_default() ), + }) +} + +struct RawMode { + needs_disable: bool, +} + +impl RawMode { + fn enable() -> io::Result { + terminal::enable_raw_mode()?; + Ok(Self { + needs_disable: true, + }) + } + fn disable(mut self) -> io::Result<()> { + self.needs_disable = false; + terminal::disable_raw_mode() } } -fn interactive() -> io::Result<()> { +impl Drop for RawMode { + fn drop(&mut self) { + if self.needs_disable { + let _ = terminal::disable_raw_mode(); + } + } +} + +pub fn select_interactive( + packages: Vec, +) -> anyhow::Result>> { let mut stdout = io::stdout(); - terminal::enable_raw_mode()?; + let raw_mode = RawMode::enable()?; - let mut state = State::new(todo!()); + let (_, rows) = terminal::size().unwrap(); + let (_, start_row) = cursor::position().unwrap_or_default(); + if rows - start_row < (packages.len() + 2) as u16 { + let pad = ((packages.len() + 2) as u16) - (rows - start_row); + + stdout.execute(terminal::ScrollUp(pad))?; + stdout.execute(cursor::MoveUp(pad))?; + } + + let mut state = State::new(packages)?; stdout.execute(cursor::Hide)?; state.render(&mut stdout)?; @@ -258,17 +326,17 @@ fn interactive() -> io::Result<()> { crossterm::queue!( &mut stdout, - terminal::Clear(terminal::ClearType::All), + cursor::MoveTo(0, state.start_row), + terminal::Clear(terminal::ClearType::FromCursorDown), cursor::Show, - cursor::MoveTo(0, 0), )?; stdout.flush()?; - terminal::disable_raw_mode()?; + raw_mode.disable()?; if do_it { - println!("doing the thing... {state:?}"); + Ok(Some(state.checked.into_iter().collect())) + } else { + Ok(None) } - - Ok(()) }