use super::*;
pub(in crate::commands::sdk) fn resolve_migrate_batch_project_dirs(
args: &[String],
) -> Result<Vec<PathBuf>, String> {
let mut project_values = Vec::new();
if let Some(raw) = option_value(args, "--projects") {
project_values.extend(split_csv_values(&raw));
}
if let Some(raw_file_path) = option_value(args, "--projects-file") {
project_values.extend(read_batch_project_values(Path::new(&raw_file_path))?);
}
if let Some(raw_scan_root) = option_value(args, "--scan-root") {
let discovered = discover_projects_from_root(Path::new(&raw_scan_root))?;
project_values.extend(
discovered
.into_iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>(),
);
}
if project_values.is_empty() {
return Err(String::from(
"missing project list (usage: sdk migrate-batch --projects <dir1,dir2,...> [--projects-file <path>] [--scan-root <dir>])",
));
}
let mut unique = HashSet::new();
let mut result = Vec::new();
for value in project_values {
let trimmed = value.trim();
if trimmed.is_empty() {
continue;
}
if unique.insert(trimmed.to_string()) {
result.push(PathBuf::from(trimmed));
}
}
if result.is_empty() {
return Err(String::from(
"no valid project directories found in --projects/--projects-file",
));
}
Ok(result)
}
pub(in crate::commands::sdk) fn discover_projects_from_root(
root: &Path,
) -> Result<Vec<PathBuf>, String> {
if !root.exists() {
return Err(format!("scan root not found: {}", root.display()));
}
if !root.is_dir() {
return Err(format!("scan root is not a directory: {}", root.display()));
}
let mut projects = Vec::new();
walk_project_dirs(root, &mut projects)?;
Ok(projects)
}
pub(in crate::commands::sdk) fn walk_project_dirs(
dir: &Path,
projects: &mut Vec<PathBuf>,
) -> Result<(), String> {
let entries = fs::read_dir(dir)
.map_err(|err| format!("read directory {} failed: {err}", dir.display()))?
.collect::<Result<Vec<_>, _>>()
.map_err(|err| format!("read directory {} failed: {err}", dir.display()))?;
let has_robotrt_config = entries.iter().any(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name == "robotrt.toml")
});
if has_robotrt_config {
projects.push(dir.to_path_buf());
return Ok(());
}
for entry in entries {
let path = entry.path();
if path.is_dir() {
walk_project_dirs(&path, projects)?;
}
}
Ok(())
}
pub(in crate::commands::sdk) fn parse_optional_u64_option(
args: &[String],
option: &str,
) -> Result<Option<u64>, String> {
let Some(raw) = option_value(args, option) else {
return Ok(None);
};
raw.parse::<u64>()
.map(Some)
.map_err(|err| format!("invalid value for {option}: {raw} ({err})"))
}
pub(in crate::commands::sdk) fn inspect_project_profile(
project_dir: &Path,
target_schema_version: u64,
) -> Result<SdkProjectProfile, String> {
let config_path = project_dir.join("robotrt.toml");
let backup_path = project_dir.join("robotrt.toml.bak");
if !config_path.exists() {
return Ok(SdkProjectProfile {
project_dir: project_dir.to_path_buf(),
schema_version: None,
template: None,
missing_config: true,
backup_conflict: backup_path.exists(),
downgrade_risk: false,
});
}
let content = fs::read_to_string(&config_path)
.map_err(|err| format!("read {} failed: {err}", config_path.display()))?;
let schema_version = parse_schema_version(&content);
let template = parse_project_template(&content);
let downgrade_risk = schema_version
.map(|value| value > target_schema_version)
.unwrap_or(false);
Ok(SdkProjectProfile {
project_dir: project_dir.to_path_buf(),
schema_version,
template,
missing_config: false,
backup_conflict: backup_path.exists(),
downgrade_risk,
})
}
pub(in crate::commands::sdk) fn split_csv_values(raw: &str) -> Vec<String> {
raw.split(',')
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToString::to_string)
.collect()
}
pub(in crate::commands::sdk) fn read_batch_project_values(
path: &Path,
) -> Result<Vec<String>, String> {
let content = fs::read_to_string(path)
.map_err(|err| format!("read project list file {} failed: {err}", path.display()))?;
Ok(content
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.map(ToString::to_string)
.collect())
}
pub(in crate::commands::sdk) fn read_json_file(path: &Path) -> Result<serde_json::Value, String> {
let content =
fs::read_to_string(path).map_err(|err| format!("read {} failed: {err}", path.display()))?;
serde_json::from_str::<serde_json::Value>(&content)
.map_err(|err| format!("parse {} failed: {err}", path.display()))
}
pub(in crate::commands::sdk) fn parse_batch_report_projects(
path: &Path,
) -> Result<Vec<BatchReportProject>, String> {
let payload = read_json_file(path)?;
let projects = payload
.get("result")
.and_then(|value| value.get("projects"))
.and_then(serde_json::Value::as_array)
.ok_or_else(|| {
format!(
"batch report {} missing result.projects array",
path.display()
)
})?;
let mut results = Vec::new();
for item in projects {
let project_dir = item
.get("project_dir")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| format!("batch report {} has invalid project_dir", path.display()))?;
let status = item
.get("status")
.and_then(serde_json::Value::as_str)
.unwrap_or("unknown")
.to_string();
let backup_path = item
.get("backup_path")
.and_then(serde_json::Value::as_str)
.map(PathBuf::from);
let error = item
.get("error")
.and_then(serde_json::Value::as_str)
.map(ToString::to_string);
results.push(BatchReportProject {
project_dir: PathBuf::from(project_dir),
status,
backup_path,
error,
});
}
Ok(results)
}
pub(in crate::commands::sdk) fn parse_batch_report_target_schema(
path: &Path,
) -> Result<Option<u64>, String> {
let payload = read_json_file(path)?;
Ok(payload
.get("query")
.and_then(|value| value.get("target_schema_version"))
.and_then(serde_json::Value::as_u64))
}
pub(in crate::commands::sdk) fn write_json_payload_file(
path: &Path,
payload: &serde_json::Value,
label: &str,
) -> Result<(), String> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|err| format!("create output dir {} failed: {err}", parent.display()))?;
}
let body = serde_json::to_string_pretty(payload)
.map_err(|err| format!("serialize {label} failed: {err}"))?;
fs::write(path, body)
.map_err(|err| format!("write {} to {} failed: {err}", label, path.display()))
}
pub(in crate::commands::sdk) fn resolve_migrate_project_dir(
args: &[String],
) -> Result<PathBuf, String> {
let project_dir = option_value(args, "--project")
.or_else(|| first_positional(args))
.ok_or_else(|| String::from("missing project dir (usage: sdk migrate <project-dir>)"))?;
let path = PathBuf::from(project_dir);
if !path.exists() {
return Err(format!("project dir not found: {}", path.display()));
}
if !path.is_dir() {
return Err(format!(
"project path is not a directory: {}",
path.display()
));
}
Ok(path)
}