use crate::audit::sections::{
BreakingChangesAuditSection, DependencyAuditSection, UpgradeAuditSection,
VersionConsistencyAuditSection, audit_dependencies as audit_dependencies_impl,
audit_upgrades as audit_upgrades_impl,
audit_version_consistency as audit_version_consistency_impl,
};
use crate::changes::ChangesAnalyzer;
use crate::changeset::{ChangesetManager, FileBasedChangesetStorage};
use crate::config::PackageToolsConfig;
use crate::error::{AuditError, AuditResult};
use crate::types::PackageInfo;
use crate::upgrade::UpgradeManager;
use std::collections::HashSet;
use std::path::PathBuf;
use sublime_git_tools::Repo;
use sublime_standard_tools::filesystem::{AsyncFileSystem, FileSystemManager};
use sublime_standard_tools::monorepo::{MonorepoDetector, MonorepoDetectorTrait};
pub struct AuditManager {
workspace_root: PathBuf,
upgrade_manager: UpgradeManager,
changes_analyzer: ChangesAnalyzer<FileSystemManager>,
changeset_manager: ChangesetManager<FileBasedChangesetStorage<FileSystemManager>>,
fs: FileSystemManager,
monorepo_detector: MonorepoDetector<FileSystemManager>,
config: PackageToolsConfig,
}
impl AuditManager {
pub async fn new(workspace_root: PathBuf, config: PackageToolsConfig) -> AuditResult<Self> {
let fs = FileSystemManager::new();
if !fs.exists(&workspace_root).await {
return Err(AuditError::InvalidWorkspaceRoot {
path: workspace_root.clone(),
reason: "Workspace root does not exist".to_string(),
});
}
let workspace_str =
workspace_root.to_str().ok_or_else(|| AuditError::InvalidWorkspaceRoot {
path: workspace_root.clone(),
reason: "Workspace path contains invalid UTF-8".to_string(),
})?;
let git_repo = Repo::open(workspace_str).map_err(|e| AuditError::GitError {
operation: "open repository".to_string(),
reason: e.to_string(),
})?;
let monorepo_detector = MonorepoDetector::new();
let _is_monorepo =
monorepo_detector.is_monorepo_root(&workspace_root).await.map_err(|e| {
AuditError::WorkspaceAnalysisFailed {
reason: format!("Failed to detect monorepo structure: {}", e),
}
})?;
let upgrade_manager = UpgradeManager::new(workspace_root.clone(), config.upgrade.clone())
.await
.map_err(|e| AuditError::UpgradeDetectionFailed {
reason: format!("Failed to initialize upgrade manager: {}", e),
})?;
let changes_analyzer =
ChangesAnalyzer::new(workspace_root.clone(), git_repo, fs.clone(), config.clone())
.await
.map_err(|e| AuditError::AnalysisFailed {
section: "changes".to_string(),
reason: format!("Failed to initialize changes analyzer: {}", e),
})?;
let changeset_manager =
ChangesetManager::new(workspace_root.clone(), fs.clone(), config.clone())
.await
.map_err(|e| AuditError::WorkspaceAnalysisFailed {
reason: format!("Failed to initialize changeset manager: {}", e),
})?;
Ok(Self {
workspace_root,
upgrade_manager,
changes_analyzer,
changeset_manager,
fs,
monorepo_detector,
config,
})
}
#[must_use]
pub fn workspace_root(&self) -> &PathBuf {
&self.workspace_root
}
#[must_use]
pub fn config(&self) -> &PackageToolsConfig {
&self.config
}
#[must_use]
pub fn upgrade_manager(&self) -> &UpgradeManager {
&self.upgrade_manager
}
#[must_use]
pub fn changes_analyzer(&self) -> &ChangesAnalyzer<FileSystemManager> {
&self.changes_analyzer
}
#[must_use]
pub fn monorepo_detector(&self) -> &MonorepoDetector<FileSystemManager> {
&self.monorepo_detector
}
#[must_use]
pub fn filesystem(&self) -> &FileSystemManager {
&self.fs
}
pub async fn audit_upgrades(&self) -> AuditResult<UpgradeAuditSection> {
audit_upgrades_impl(&self.upgrade_manager, &self.config).await
}
pub async fn audit_dependencies(&self) -> AuditResult<DependencyAuditSection> {
let packages = self.discover_packages().await?;
audit_dependencies_impl(&self.workspace_root, &packages, &self.config).await
}
pub async fn categorize_dependencies(
&self,
) -> AuditResult<crate::audit::sections::DependencyCategorization> {
let packages = self.discover_packages().await?;
crate::audit::sections::categorize_dependencies(&packages, &self.config).await
}
pub async fn audit_version_consistency(&self) -> AuditResult<VersionConsistencyAuditSection> {
let packages = self.discover_packages().await?;
let internal_package_names: HashSet<String> =
packages.iter().map(|p| p.name().to_string()).collect();
audit_version_consistency_impl(&packages, &internal_package_names, &self.config).await
}
async fn discover_packages(&self) -> AuditResult<Vec<PackageInfo>> {
let monorepo_kind =
self.monorepo_detector.is_monorepo_root(&self.workspace_root).await.map_err(|e| {
AuditError::WorkspaceAnalysisFailed {
reason: format!("Failed to detect monorepo: {}", e),
}
})?;
if monorepo_kind.is_some() {
let monorepo =
self.monorepo_detector.detect_monorepo(&self.workspace_root).await.map_err(
|e| AuditError::WorkspaceAnalysisFailed {
reason: format!("Failed to detect monorepo: {}", e),
},
)?;
let workspace_packages = monorepo.packages();
if workspace_packages.is_empty() {
return Err(AuditError::PackageNotFound { package: "any package".to_string() });
}
let mut packages = Vec::with_capacity(workspace_packages.len());
for workspace_package in workspace_packages {
let package_json_path = workspace_package.absolute_path.join("package.json");
let content = self.fs.read_file_string(&package_json_path).await.map_err(|e| {
AuditError::FileSystemError {
path: package_json_path.clone(),
reason: format!("Failed to read file: {}", e),
}
})?;
let package_json: package_json::PackageJson = serde_json::from_str(&content)
.map_err(|e| AuditError::FileSystemError {
path: package_json_path.clone(),
reason: format!("Failed to parse JSON: {}", e),
})?;
packages.push(PackageInfo::new(
package_json,
Some(workspace_package.clone()),
workspace_package.absolute_path.clone(),
));
}
Ok(packages)
} else {
let package_json_path = self.workspace_root.join("package.json");
let content = self.fs.read_file_string(&package_json_path).await.map_err(|e| {
AuditError::FileSystemError {
path: package_json_path.clone(),
reason: format!("Failed to read package.json: {}", e),
}
})?;
let package_json: package_json::PackageJson =
serde_json::from_str(&content).map_err(|e| AuditError::FileSystemError {
path: package_json_path.clone(),
reason: format!("Failed to parse package.json: {}", e),
})?;
Ok(vec![PackageInfo::new(package_json, None, self.workspace_root.clone())])
}
}
pub async fn audit_breaking_changes(&self) -> AuditResult<BreakingChangesAuditSection> {
use crate::audit::sections::audit_breaking_changes;
let head_commit = "HEAD";
let base_commit = self.determine_base_commit()?;
let pending_changesets = self.changeset_manager.list_pending().await.map_err(|e| {
AuditError::WorkspaceAnalysisFailed {
reason: format!("Failed to load pending changesets: {}", e),
}
})?;
let changeset = pending_changesets.first();
audit_breaking_changes(
&self.changes_analyzer,
&base_commit,
head_commit,
changeset,
&self.config.audit.breaking_changes,
)
.await
}
fn determine_base_commit(&self) -> AuditResult<String> {
if let Ok(last_tag) = self.changes_analyzer.git_repo().get_last_tag() {
return Ok(last_tag);
}
if let Ok(current_branch) = self.changes_analyzer.git_repo().get_current_branch() {
for main_branch in &["main", "master"] {
if let Ok(merge_base) =
self.changes_analyzer.git_repo().get_merge_base(¤t_branch, main_branch)
{
return Ok(merge_base);
}
}
}
Ok("HEAD~10".to_string())
}
}