use clap::Args; use indexmap::{IndexMap, IndexSet}; use mozart_core::console::IoInterface; use mozart_core::console_format; use mozart_core::console_writeln; use mozart_core::package::{self, RawPackageData, Stability}; use mozart_core::repository::lockfile; use mozart_core::repository::packagist; use mozart_core::repository::resolver::{self, PlatformConfig, ResolveRequest}; use mozart_core::repository::version; use mozart_core::repository::version_selector::VersionSelector; use mozart_core::validation; use std::io::{BufRead as _, IsTerminal as _, Write as _}; use std::path::{Path, PathBuf}; #[derive(Args)] pub struct RequireArgs { /// Package(s) to require pub packages: Vec, /// Add requirement to require-dev #[arg(long)] pub dev: bool, /// Only output what would be changed, do not modify files #[arg(long)] pub dry_run: bool, /// Forces installation from package sources when possible #[arg(long)] pub prefer_source: bool, /// Forces installation from package dist #[arg(long)] pub prefer_dist: bool, /// Forces usage of a specific install method (dist, source, auto) #[arg(long)] pub prefer_install: Option, /// Pin the exact version instead of a range #[arg(long)] pub fixed: bool, /// [Deprecated] Do not show install suggestions #[arg(long)] pub no_suggest: bool, /// Do not output download progress #[arg(long)] pub no_progress: bool, /// Disables the automatic update of the lock file #[arg(long)] pub no_update: bool, /// Skip the install step #[arg(long)] pub no_install: bool, /// Skip the audit step #[arg(long)] pub no_audit: bool, /// Audit output format #[arg(long, value_parser = ["table", "plain", "json", "summary"])] pub audit_format: Option, /// Do not block on security advisories #[arg(long)] pub no_security_blocking: bool, /// Run the dependency update with the --no-dev option #[arg(long)] pub update_no_dev: bool, /// [Deprecated] Use --with-dependencies instead #[arg(short = 'w', long)] pub update_with_dependencies: bool, /// [Deprecated] Use --with-all-dependencies instead #[arg(short = 'W', long)] pub update_with_all_dependencies: bool, /// Update also dependencies of newly required packages #[arg(long)] pub with_dependencies: bool, /// Update all dependencies including root requirements #[arg(long)] pub with_all_dependencies: bool, /// Ignore a specific platform requirement #[arg(long)] pub ignore_platform_req: Vec, /// Ignore all platform requirements #[arg(long)] pub ignore_platform_reqs: bool, /// Prefer stable versions of dependencies #[arg(long)] pub prefer_stable: bool, /// Prefer lowest versions of dependencies #[arg(long)] pub prefer_lowest: bool, /// Prefer minimal restriction updates #[arg(short = 'm', long)] pub minimal_changes: bool, /// Sort packages in composer.json #[arg(long)] pub sort_packages: bool, /// Optimizes PSR-0 and PSR-4 packages to be loaded with classmaps #[arg(short, long)] pub optimize_autoloader: bool, /// Autoload classes from the classmap only #[arg(short = 'a', long)] pub classmap_authoritative: bool, /// Use APCu to cache found/not-found classes #[arg(long)] pub apcu_autoloader: bool, /// Use a custom prefix for the APCu autoloader cache #[arg(long)] pub apcu_autoloader_prefix: Option, } /// Per-execution mutable state. /// Mirrors Composer\Command\RequireCommand instance properties. struct CommandState { newly_created: bool, first_require: bool, json_path: PathBuf, lock_path: PathBuf, composer_backup: String, lock_backup: Option, dependency_resolution_completed: bool, } /// Reverts composer.json (and composer.lock) to their pre-command state. /// Mirrors Composer\Command\RequireCommand::revertComposerFile(). fn revert_composer_file( state: &CommandState, io: std::sync::Arc>>, ) { if state.newly_created { io.lock().unwrap().write_error(&format!( "\nInstallation failed, deleting {}.", state.json_path.display() )); if let Err(e) = std::fs::remove_file(&state.json_path) { io.lock().unwrap().write_error(&format!( "Warning: Failed to delete {}: {e}", state.json_path.display() )); } // Also remove any lock file that was created during this (failed) run if state.lock_path.exists() && let Err(e) = std::fs::remove_file(&state.lock_path) { io.lock().unwrap().write_error(&format!( "Warning: Failed to delete {}: {e}", state.lock_path.display() )); } } else { let msg = if state.lock_backup.is_some() { format!(" and {} to their", state.lock_path.display()) } else { " to its".to_string() }; io.lock().unwrap().write_error(&format!( "\nInstallation failed, reverting {}{msg} original content.", state.json_path.display() )); if let Err(e) = std::fs::write(&state.json_path, &state.composer_backup) { io.lock().unwrap().write_error(&format!( "Warning: Failed to revert {}: {e}", state.json_path.display() )); } if let Some(ref lock_content) = state.lock_backup && let Err(e) = std::fs::write(&state.lock_path, lock_content) { io.lock().unwrap().write_error(&format!( "Warning: Failed to revert {}: {e}", state.lock_path.display() )); } } } /// Returns the names of packages that are being added to `require_key` but already /// live in the opposite section. /// Mirrors Composer\Command\RequireCommand::getInconsistentRequireKeys(). fn get_inconsistent_require_keys( new_packages: &[String], require_key: &str, packages_by_key: &IndexMap, ) -> Vec { new_packages .iter() .filter(|name| { packages_by_key .get(name.as_str()) .map(|k| k != require_key) .unwrap_or(false) }) .cloned() .collect() } /// Returns a map of `package_name → "require" | "require-dev"` for all existing packages. /// Mirrors Composer\Command\RequireCommand::getPackagesByRequireKey(). fn get_packages_by_require_key(raw: &RawPackageData) -> IndexMap { let mut map = IndexMap::new(); for name in raw.require.keys() { map.insert(name.clone(), "require".to_string()); } for name in raw.require_dev.keys() { map.insert(name.clone(), "require-dev".to_string()); } map } /// Formatting-preserving composer.json write (stub — returns `Ok(false)` to trigger fallback). /// Mirrors Composer\Command\RequireCommand::updateFileCleanly(). /// Will be implemented in PR 3 when JsonManipulator is ported. fn update_file_cleanly(_json_path: &Path, _raw: &RawPackageData) -> anyhow::Result { Ok(false) } /// Write the updated requirements to composer.json. /// Tries the formatting-preserving path first; falls back to a full rewrite. /// Mirrors Composer\Command\RequireCommand::updateFile(). fn update_file(json_path: &Path, raw: &RawPackageData) -> anyhow::Result<()> { if update_file_cleanly(json_path, raw)? { return Ok(()); } package::write_to_file(raw, json_path) } /// Post-resolution constraint rewrite for `'guess'` placeholders (stub for PR 2). /// Mirrors Composer\Command\RequireCommand::updateRequirementsAfterResolution(). #[allow(clippy::too_many_arguments)] async fn update_requirements_after_resolution( _state: &CommandState, _requirements_to_update: &[String], _require_key: &str, _remove_key: &str, _sort_packages: bool, _dry_run: bool, _fixed: bool, _io: std::sync::Arc>>, ) -> anyhow::Result<()> { Ok(()) } /// Resolve + lock + install pipeline. /// Mirrors Composer\Command\RequireCommand::doUpdate(). async fn do_update( state: &mut CommandState, args: &RequireArgs, cli: &super::Cli, raw: &RawPackageData, additions: &[(String, String, bool)], io: &std::sync::Arc>>, ) -> anyhow::Result<()> { let working_dir = cli.working_dir()?; let vendor_dir = working_dir.join("vendor"); let cache_config = mozart_core::repository::cache::build_cache_config(cli.no_cache); let repo_cache = mozart_core::repository::cache::Cache::repo(&cache_config); let dev_mode = !args.update_no_dev; let require: Vec<(String, String)> = raw .require .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); let require_dev: Vec<(String, String)> = raw .require_dev .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); let minimum_stability = package::Stability::parse(raw.minimum_stability.as_deref().unwrap_or("stable")); let prefer_stable = args.prefer_stable || raw .extra_fields .get("prefer-stable") .and_then(|v| v.as_bool()) .unwrap_or(false); // Audit: wire --no-security-blocking + COMPOSER_NO_SECURITY_BLOCKING env var. // Mirrors BaseCommand::createAuditConfig() + Installer::setAuditConfig(). let no_security_blocking = args.no_security_blocking || std::env::var("COMPOSER_NO_SECURITY_BLOCKING") .map(|v| v != "0" && !v.is_empty()) .unwrap_or(false); let no_audit = args.no_audit || std::env::var("COMPOSER_NO_AUDIT") .map(|v| v != "0" && !v.is_empty()) .unwrap_or(false); let block_insecure = !no_audit && !no_security_blocking; let request = ResolveRequest { root_name: raw.name.clone(), root_version: raw.version.clone(), require, require_dev, include_dev: dev_mode, minimum_stability, stability_flags: IndexMap::new(), prefer_stable, prefer_lowest: args.prefer_lowest, platform: PlatformConfig::new(), ignore_platform_reqs: args.ignore_platform_reqs, ignore_platform_req_list: args.ignore_platform_req.clone(), repositories: std::sync::Arc::new( mozart_core::repository::repository::RepositorySet::with_packagist(repo_cache.clone()), ), temporary_constraints: IndexMap::new(), raw_repositories: raw.repositories.clone(), root_provide: raw .provide .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(), root_replace: raw .replace .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(), root_conflict: raw .conflict .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(), locked_package_names: IndexSet::new(), locked_packages: Vec::new(), block_abandoned: false, root_branch_alias: None, preferred_versions: IndexMap::new(), block_insecure, }; io.lock() .unwrap() .info("Loading composer repositories with package information"); if dev_mode { io.lock() .unwrap() .info("Updating dependencies (including require-dev)"); } else { io.lock().unwrap().info("Updating dependencies"); } io.lock().unwrap().info("Resolving dependencies..."); let mut resolved = match resolver::resolve(&request).await { Ok(packages) => packages, Err(e) => { if !args.dry_run { revert_composer_file(state, io.clone()); } // Suggest explicit version constraint retry for the first package without one. // Mirrors Composer\Command\RequireCommand::doUpdate() L496-502. let first_unversioned = additions .iter() .find(|(_, constraint, _)| { !constraint.contains(['^', '~', '>', '<', '!', '=', '*']) }) .map(|(name, _, _)| name.as_str()); let hint = if let Some(name) = first_unversioned { format!( "\n\nYou can also try re-running mozart require with an explicit version \ constraint, e.g. \"mozart require {name}:*\" to figure out if any version \ is installable, or \"mozart require {name}:^2.1\" if you know which you need." ) } else { String::new() }; return Err(mozart_core::exit_code::bail( mozart_core::exit_code::DEPENDENCY_RESOLUTION_FAILED, format!("{e}{hint}"), )); } }; state.dependency_resolution_completed = true; // Read old lock file for change reporting and partial update pinning. let old_lock = if state.lock_path.exists() { match lockfile::LockFile::read_from_file(&state.lock_path) { Ok(l) => Some(l), Err(e) => { io.lock().unwrap().info(&console_format!( "Could not read existing composer.lock: {e}. \ Treating as a fresh install." )); None } } } else { None }; // Apply setUpdateAllowList only when NOT firstRequire and lock exists. // Mirrors Composer\Command\RequireCommand::doUpdate() L490-492: // if (!$this->firstRequire && $composer->getLocker()->isLocked()) // $install->setUpdateAllowList(array_keys($requirements)); if !state.first_require && let Some(ref lock) = old_lock { let with_deps = args.with_dependencies || args.update_with_dependencies; let with_all_deps = args.with_all_dependencies || args.update_with_all_dependencies; let newly_required: Vec = additions.iter().map(|(name, _, _)| name.clone()).collect(); let repo_requires = super::update::collect_repo_requires(&raw.repositories); let allow_list = if with_all_deps { super::update::expand_with_all_dependencies(newly_required, lock, &repo_requires) } else if with_deps { super::update::expand_with_direct_dependencies( newly_required, lock, &IndexSet::new(), &repo_requires, ) } else { additions.iter().map(|(name, _, _)| name.clone()).collect() }; resolved = super::update::apply_partial_update(resolved, lock, &allow_list); } let composer_json_content = if args.dry_run { package::to_json_pretty(raw)? } else { std::fs::read_to_string(&state.json_path)? }; let new_lock = lockfile::generate_lock_file(&lockfile::LockFileGenerationRequest { resolved_packages: resolved, composer_json_content: composer_json_content.clone(), composer_json: raw.clone(), include_dev: dev_mode, repositories: std::sync::Arc::new( mozart_core::repository::repository::RepositorySet::with_packagist(repo_cache.clone()), ), previous_lock: old_lock.clone(), lock_pinned_names: IndexSet::new(), }) .await?; // Compute and print change report. let changes = super::update::compute_update_changes(old_lock.as_ref(), &new_lock, dev_mode); let installs: Vec<_> = changes .iter() .filter(|c| matches!(c.kind, super::update::ChangeKind::Install { .. })) .collect(); let updates: Vec<_> = changes .iter() .filter(|c| matches!(c.kind, super::update::ChangeKind::Update { .. })) .collect(); let removals: Vec<_> = changes .iter() .filter(|c| matches!(c.kind, super::update::ChangeKind::Uninstall { .. })) .collect(); io.lock().unwrap().info(&format!( "Package operations: {} install{}, {} update{}, {} removal{}", installs.len(), if installs.len() == 1 { "" } else { "s" }, updates.len(), if updates.len() == 1 { "" } else { "s" }, removals.len(), if removals.len() == 1 { "" } else { "s" }, )); for change in &changes { match &change.kind { super::update::ChangeKind::Uninstall { old_version } => { if args.dry_run { io.lock() .unwrap() .info(&format!(" - Would remove {} ({old_version})", change.name)); } else { io.lock() .unwrap() .info(&format!(" - Removing {} ({old_version})", change.name)); } } super::update::ChangeKind::Install { new_version } => { if args.dry_run { io.lock().unwrap().info(&format!( " - Would install {} ({new_version})", change.name )); } else { io.lock() .unwrap() .info(&format!(" - Installing {} ({new_version})", change.name)); } } super::update::ChangeKind::Update { old_version, new_version, } => { if args.dry_run { io.lock().unwrap().info(&format!( " - Would update {} ({old_version} => {new_version})", change.name )); } else { io.lock().unwrap().info(&format!( " - Updating {} ({old_version} => {new_version})", change.name )); } } } } if !args.dry_run { io.lock().unwrap().info("Writing lock file"); new_lock.write_to_file(&state.lock_path)?; } if !args.no_install && !args.dry_run { let prefer_source = args.prefer_source || args .prefer_install .as_deref() .map(|s| s.eq_ignore_ascii_case("source")) .unwrap_or(false); if prefer_source { io.lock().unwrap().info(&console_format!( "Warning: Source installs are not yet supported. \ Falling back to dist." )); } let composer_config = raw.extra_fields.get("config"); let config_optimize = composer_config .and_then(|c| c.get("optimize-autoloader")) .and_then(|v| v.as_bool()) .unwrap_or(false); let config_classmap = composer_config .and_then(|c| c.get("classmap-authoritative")) .and_then(|v| v.as_bool()) .unwrap_or(false); let config_apcu = composer_config .and_then(|c| c.get("apcu-autoloader")) .and_then(|v| v.as_bool()) .unwrap_or(false); let files_cache = mozart_core::repository::cache::Cache::files( &mozart_core::repository::cache::build_cache_config(cli.no_cache), ); let mut executor = mozart_core::repository::installer_executor::FilesystemExecutor::new(files_cache); super::install::install_from_lock( &new_lock, &working_dir, &vendor_dir, &super::install::InstallConfig { dev_mode, dry_run: false, no_autoloader: false, no_progress: args.no_progress, ignore_platform_reqs: args.ignore_platform_reqs, ignore_platform_req: args.ignore_platform_req.clone(), optimize_autoloader: args.optimize_autoloader || config_optimize, classmap_authoritative: args.classmap_authoritative || config_classmap, apcu_autoloader: args.apcu_autoloader || args.apcu_autoloader_prefix.is_some() || config_apcu, apcu_autoloader_prefix: args.apcu_autoloader_prefix.clone(), download_only: false, prefer_source: args.prefer_source, }, io.clone(), &mut executor, ) .await?; } Ok(()) } /// Run the interactive package search+pick loop. /// /// Returns a list of `"vendor/package:constraint"` strings that the user confirmed, /// or an empty vec if the user typed nothing / pressed Ctrl-D immediately. async fn interactive_search_packages( already_required: &indexmap::IndexSet, preferred_stability: Stability, fixed: bool, repo_cache: &mozart_core::repository::cache::Cache, io: &std::sync::Arc>>, ) -> anyhow::Result> { let stdin = std::io::stdin(); if !stdin.is_terminal() { anyhow::bail!( "Not enough arguments (missing: \"packages\") and stdin is not a TTY. \ Pass package name(s) directly or run interactively." ); } let mut selected: Vec = Vec::new(); loop { eprint!("Search for a package: "); let _ = std::io::stderr().flush(); let query = { let stdin_locked = stdin.lock(); let mut lines = stdin_locked.lines(); match lines.next() { Some(Ok(line)) => line.trim().to_string(), _ => break, } }; if query.is_empty() { break; } let (results, total) = match packagist::search_packages(&query, None).await { Ok(r) => r, Err(e) => { io.lock().unwrap().info(&console_format!( "Search failed: {e}. Try again." )); continue; } }; let filtered: Vec<&packagist::SearchResult> = results .iter() .filter(|r| !already_required.contains(&r.name.to_lowercase())) .take(15) .collect(); if filtered.is_empty() { io.lock().unwrap().info(&console_format!( "No new packages found for \"{query}\" (total: {total})." )); continue; } io.lock().unwrap().info(&format!( "\nFound {} package{} for \"{}\":", filtered.len(), if filtered.len() == 1 { "" } else { "s" }, query )); let name_width = filtered.iter().map(|r| r.name.len()).max().unwrap_or(0); for (idx, result) in filtered.iter().enumerate() { let desc = if result.description.is_empty() { String::new() } else { format!(" — {}", result.description) }; io.lock().unwrap().info(&format!( " [{idx}] {: line.trim().to_string(), _ => break, } }; if choice.is_empty() { break; } let package_name: String = if let Ok(num) = choice.parse::() { if num == 0 { continue; } else if num <= filtered.len() { filtered[num - 1].name.to_lowercase() } else { io.lock().unwrap().info(&console_format!( "Invalid selection: {num}" )); continue; } } else { choice.to_lowercase() }; let (pkg_name, constraint) = if package_name.contains(':') { match validation::parse_require_string(&package_name) { Ok((n, v)) => (n.to_lowercase(), v), Err(e) => { io.lock() .unwrap() .info(&console_format!("Invalid: {e}")); continue; } } } else { if !validation::validate_package_name(&package_name) { io.lock().unwrap().info(&console_format!( "Invalid package name: \"{package_name}\"" )); continue; } io.lock().unwrap().info(&console_format!( "Using version constraint for {package_name} from Packagist..." )); match packagist::fetch_package_versions(&package_name, repo_cache).await { Ok(versions) => { match version::find_best_candidate(&versions, preferred_stability) { Some(best) => { let stability = version::stability_of(&best.version_normalized); let c = if fixed { best.version.clone() } else { version::find_recommended_require_version( &best.version, &best.version_normalized, stability, ) }; io.lock().unwrap().info(&console_format!( "Using version {c} for {package_name}" )); (package_name, c) } None => { io.lock().unwrap().info(&console_format!( "Could not find a version of \"{package_name}\" \ matching your minimum-stability. Try specifying it \ explicitly." )); continue; } } } Err(e) => { io.lock().unwrap().info(&console_format!( "Could not fetch versions for \"{package_name}\": \ {e}" )); continue; } } }; selected.push(format!("{pkg_name}:{constraint}")); eprint!("Search for another package? [y/N] "); let _ = std::io::stderr().flush(); let again = { let stdin_locked = stdin.lock(); let mut lines = stdin_locked.lines(); match lines.next() { Some(Ok(line)) => line.trim().to_lowercase(), _ => break, } }; if again != "y" && again != "yes" { break; } } Ok(selected) } pub async fn execute( args: &RequireArgs, cli: &super::Cli, io: std::sync::Arc>>, ) -> anyhow::Result<()> { let cache_config = mozart_core::repository::cache::build_cache_config(cli.no_cache); let repo_cache = mozart_core::repository::cache::Cache::repo(&cache_config); // --- Deprecated flag warnings --- // Mirrors Composer\Command\RequireCommand::execute() L134-136. if args.no_suggest { io.lock().unwrap().write_error(&console_format!( "You are using the deprecated option \"--no-suggest\". \ It has no effect and will break in Composer 3." )); } if args.update_with_dependencies { io.lock().unwrap().write_error(&console_format!( "The -w / --update-with-dependencies flag is deprecated. \ Use --with-dependencies instead." )); } if args.update_with_all_dependencies { io.lock().unwrap().write_error(&console_format!( "The -W / --update-with-all-dependencies flag is deprecated. \ Use --with-all-dependencies instead." )); } // --- Collect package arguments (interactive if none given) --- let cli_packages: Vec = if args.packages.is_empty() { if cli.no_interaction { anyhow::bail!("Not enough arguments (missing: \"packages\")."); } let working_dir = cli.working_dir()?; let composer_path = working_dir.join("composer.json"); // Read current dependencies to filter from search results (best-effort). let (already_required, preferred_stability) = if composer_path.exists() { let raw_check = package::read_from_file(&composer_path)?; let mut already: IndexSet = IndexSet::new(); for k in raw_check.require.keys() { already.insert(k.to_lowercase()); } for k in raw_check.require_dev.keys() { already.insert(k.to_lowercase()); } let stab = raw_check .minimum_stability .as_deref() .map(Stability::parse) .unwrap_or(Stability::Stable); (already, stab) } else { (IndexSet::new(), Stability::Stable) }; let found = interactive_search_packages( &already_required, preferred_stability, args.fixed, &repo_cache, &io, ) .await?; if found.is_empty() { return Ok(()); } found } else { args.packages.clone() }; let working_dir = cli.working_dir()?; let composer_path = working_dir.join("composer.json"); // --- Bootstrap composer.json --- // Mirrors Composer\Command\RequireCommand::execute() L138-152. let newly_created = !composer_path.exists(); if newly_created { if let Err(e) = std::fs::write(&composer_path, "{\n}\n") { anyhow::bail!("{} could not be created: {e}", composer_path.display()); } } else if std::fs::metadata(&composer_path) .map(|m| m.len() == 0) .unwrap_or(false) { std::fs::write(&composer_path, "{\n}\n")?; } // Backup original content (including the bootstrap content for new files). let composer_backup = std::fs::read_to_string(&composer_path)?; let lock_path = working_dir.join("composer.lock"); let lock_backup = if lock_path.exists() { Some(std::fs::read_to_string(&lock_path)?) } else { None }; // Read and parse composer.json. let mut raw = package::read_from_file(&composer_path)?; // --- firstRequire: computed from the original file, before applying changes --- // Mirrors Composer\Command\RequireCommand::execute() L315-321. let first_require = newly_created || (raw.require.is_empty() && raw.require_dev.is_empty()); let mut state = CommandState { newly_created, first_require, json_path: composer_path.clone(), lock_path: lock_path.clone(), composer_backup, lock_backup, dependency_resolution_completed: false, }; // --- --fixed gate --- // Mirrors Composer\Command\RequireCommand::execute() L173-189. if args.fixed { let package_type = raw .package_type .as_deref() .filter(|t| !t.is_empty()) .unwrap_or("library"); if package_type != "project" && !args.dev { io.lock().unwrap().write_error(&console_format!( "The \"--fixed\" option is only allowed for packages with a \ \"project\" type or for dev dependencies to prevent possible \ misuses." )); if raw.package_type.is_none() { io.lock().unwrap().write_error(&console_format!( "If your package is not a library, you can explicitly specify \ the \"type\" by using \"mozart config type project\"." )); } return Err(mozart_core::exit_code::bail( mozart_core::exit_code::GENERAL_ERROR, String::new(), )); } } // --- preferred_stability --- let preferred_stability = raw .minimum_stability .as_deref() .map(Stability::parse) .unwrap_or(Stability::Stable); let require_key = if args.dev { "require-dev" } else { "require" }; let remove_key = if args.dev { "require" } else { "require-dev" }; // --- Per-arg constraint resolution via VersionSelector --- // Mirrors Composer\Command\PackageDiscoveryTrait::determineRequirements(). let version_selector = VersionSelector::new(preferred_stability, repo_cache.clone()); let mut additions: Vec<(String, String, bool)> = Vec::new(); for pkg_arg in &cli_packages { let (name, constraint) = match validation::parse_require_string(pkg_arg) { Ok((n, v)) => (n.to_lowercase(), v), Err(_) => { let name = pkg_arg.trim().to_lowercase(); if !validation::validate_package_name(&name) { anyhow::bail!("Invalid package name: \"{name}\""); } console_writeln!( io, "Using version constraint for {name} from Packagist..." ); let best = version_selector .find_best_candidate(&name) .await? .ok_or_else(|| { anyhow::anyhow!( "Could not find a version of package \"{name}\" matching your \ minimum-stability ({preferred_stability:?}). Try requiring it \ with an explicit version constraint." ) })?; let constraint = version_selector.find_recommended_require_version_string(&best, args.fixed); console_writeln!(io, "Using version {constraint} for {name}",); (name, constraint) } }; additions.push((name, constraint, args.dev)); } // --- Self-require detection --- // Mirrors Composer\Command\RequireCommand::execute() L278-282. let root_name = raw.name.to_lowercase(); for (name, _, _) in &additions { if name.to_lowercase() == root_name { anyhow::bail!( "Root package '{}' cannot require itself in its composer.json", raw.name ); } } // --- Inconsistent require-key detection + warning --- // Mirrors Composer\Command\RequireCommand::execute() L289-311. let packages_by_key = get_packages_by_require_key(&raw); let new_package_names: Vec = additions.iter().map(|(n, _, _)| n.clone()).collect(); let inconsistent = get_inconsistent_require_keys(&new_package_names, require_key, &packages_by_key); for pkg in &inconsistent { let (with_without, target_key) = if args.dev { ("with", require_key) } else { ("without", require_key) }; io.lock().unwrap().write_error(&console_format!( "{pkg} is currently present in the {remove_key} key and you ran the \ command {with_without} the --dev flag, which will move it to the \ {target_key} key." )); } // Remove from the opposite section before inserting into the target. for pkg in &inconsistent { if args.dev { raw.require.shift_remove(pkg.as_str()); } else { raw.require_dev.shift_remove(pkg.as_str()); } } // --- Apply changes --- for (name, constraint, is_dev) in &additions { let section_name = if *is_dev { "require-dev" } else { "require" }; let target = if *is_dev { &mut raw.require_dev } else { &mut raw.require }; if let Some(existing) = target.get(name) { console_writeln!( io, "Updating {name} from {existing} to {constraint} in {section_name}", ); } else { console_writeln!( io, "Adding {name} ({constraint}) to {section_name}", ); } target.insert(name.clone(), constraint.clone()); } // --- sort-packages --- let config_sort_packages = raw .extra_fields .get("config") .and_then(|c| c.get("sort-packages")) .and_then(|v| v.as_bool()) .unwrap_or(false); let sort_packages = args.sort_packages || config_sort_packages; if sort_packages { raw.require.sort_unstable_keys(); raw.require_dev.sort_unstable_keys(); } // --- Write composer.json (unless --dry-run) --- // Mirrors Composer\Command\RequireCommand::execute() L323-325. if args.dry_run { console_writeln!( io, "Dry run: composer.json not modified.", ); } else { update_file(&composer_path, &raw)?; } // Print "has been created|updated". // Mirrors Composer\Command\RequireCommand::execute() L327. io.lock().unwrap().info(&console_format!( "{} has been {}", composer_path.display(), if newly_created { "created" } else { "updated" } )); // --- --no-update: skip resolution --- if args.no_update { console_writeln!( io, "Not updating dependencies, only modifying composer.json." ); return Ok(()); } // --- Resolution + lock + install --- let update_result = do_update(&mut state, args, cli, &raw, &additions, &io).await; // Mirrors Composer's `finally` block: cleanup newly-created file on dry-run. if args.dry_run && state.newly_created { let _ = std::fs::remove_file(&state.json_path); } update_result?; // --- Post-resolution constraint rewrite for 'guess' placeholders (stub, PR 2) --- update_requirements_after_resolution( &state, &[], require_key, remove_key, sort_packages, args.dry_run, args.fixed, io, ) .await?; Ok(()) }