use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::path::Path;
use monochange_core::DependencySyncChange;
use monochange_core::Ecosystem;
use monochange_core::MonochangeError;
use monochange_core::MonochangeResult;
use monochange_core::VersionStrategy;
use crate::discover_workspace;
#[derive(Debug)]
pub struct SyncResult {
pub changes: Vec<FileSyncResult>,
}
#[derive(Debug)]
pub struct FileSyncResult {
pub path: String,
pub changes: Vec<DependencySyncChange>,
}
pub fn sync_workspace_versions(
root: &Path,
strategy: VersionStrategy,
dry_run: bool,
) -> MonochangeResult<SyncResult> {
let discovery = discover_workspace(root)?;
let mut all_changes: Vec<FileSyncResult> = Vec::new();
let version_map: BTreeMap<String, String> = discovery
.packages
.iter()
.filter_map(|package| {
package
.current_version
.as_ref()
.map(|v| (package.name.clone(), v.to_string()))
})
.collect();
if version_map.is_empty() {
return Ok(SyncResult {
changes: Vec::new(),
});
}
let workspace_package_names: BTreeSet<String> =
discovery.packages.iter().map(|p| p.name.clone()).collect();
for package in &discovery.packages {
let ecosystem = package.ecosystem;
let changes = match ecosystem {
Ecosystem::Dart => {
let manifest_path = root.join(&package.manifest_path);
let contents = read_manifest(&manifest_path)?;
dart_changes(&contents, &version_map, &workspace_package_names, strategy)?
}
Ecosystem::Npm => {
let manifest_path = root.join(&package.manifest_path);
let contents = read_manifest(&manifest_path)?;
npm_changes(&contents, &version_map, &workspace_package_names, strategy)?
}
_ => Vec::new(),
};
if changes.is_empty() {
continue;
}
if !dry_run {
let manifest_path = root.join(&package.manifest_path);
let contents = read_manifest(&manifest_path)?;
let updated_contents = apply_sync_changes(&contents, &changes, ecosystem)?;
write_manifest(&manifest_path, updated_contents)?;
}
all_changes.push(FileSyncResult {
path: package.manifest_path.to_string_lossy().to_string(),
changes,
});
}
Ok(SyncResult {
changes: all_changes,
})
}
pub(crate) fn dart_changes(
contents: &str,
version_map: &BTreeMap<String, String>,
workspace_package_names: &BTreeSet<String>,
strategy: VersionStrategy,
) -> MonochangeResult<Vec<DependencySyncChange>> {
monochange_dart::sync_internal_dependency_versions(
contents,
version_map,
workspace_package_names,
strategy,
)
}
fn npm_changes(
contents: &str,
version_map: &BTreeMap<String, String>,
workspace_package_names: &BTreeSet<String>,
strategy: VersionStrategy,
) -> MonochangeResult<Vec<DependencySyncChange>> {
monochange_npm::sync_internal_dependency_versions(
contents,
version_map,
workspace_package_names,
strategy,
)
}
pub(crate) fn read_manifest(path: &Path) -> MonochangeResult<String> {
std::fs::read_to_string(path)
.map_err(|error| MonochangeError::Io(format!("failed to read {}: {error}", path.display())))
}
pub(crate) fn write_manifest(path: &Path, contents: String) -> MonochangeResult<()> {
std::fs::write(path, contents).map_err(|error| {
MonochangeError::Io(format!("failed to write {}: {error}", path.display()))
})
}
pub(crate) fn apply_sync_changes(
contents: &str,
changes: &[DependencySyncChange],
ecosystem: Ecosystem,
) -> MonochangeResult<String> {
let versioned_deps: BTreeMap<String, String> = changes
.iter()
.map(|c| (c.dependency_name.clone(), c.new_value.clone()))
.collect();
match ecosystem {
Ecosystem::Dart => {
let fields = monochange_dart::default_dependency_fields();
monochange_dart::update_manifest_text(contents, None, fields, &versioned_deps)
}
Ecosystem::Npm => {
let fields = monochange_npm::default_dependency_fields();
monochange_core::update_json_manifest_text(contents, None, fields, &versioned_deps)
}
_ => Ok(contents.to_string()),
}
}
pub(crate) fn format_sync_result(result: &SyncResult, dry_run: bool, quiet: bool) -> String {
if result.changes.is_empty() {
if !quiet {
eprintln!("No changes needed. All internal dependency versions are already in sync.");
}
return String::new();
}
let mut output = String::new();
for file_result in &result.changes {
for change in &file_result.changes {
let line = if dry_run {
format!(
"would update {} → {} in {} ({})\n",
change.old_value, change.new_value, change.dependency_name, file_result.path
)
} else {
format!(
"updated {} → {} in {} ({})\n",
change.old_value, change.new_value, change.dependency_name, file_result.path
)
};
if !quiet {
eprint!("{line}");
}
output.push_str(&line);
}
}
if dry_run {
output.push_str("\n(dry run — no files were modified)\n");
}
output
}
pub(crate) fn parse_strategy(strategy_str: &str) -> VersionStrategy {
match strategy_str {
"exact" => VersionStrategy::Exact,
"caret" => VersionStrategy::Caret,
"compatible" => VersionStrategy::Compatible,
_ => VersionStrategy::Default,
}
}