use std::fs;
use std::path::Path;
use super::{AreaResult, MigrationArea, err, qt_ml};
pub(crate) fn import_personality(oc_root: &Path, ic_root: &Path) -> AreaResult {
let ws = oc_root.join("workspace");
let soul_path = ws.join("SOUL.md");
let agents_path = ws.join("AGENTS.md");
let out_dir = ic_root.join("workspace");
if let Err(e) = fs::create_dir_all(&out_dir) {
return err(
MigrationArea::Personality,
format!("Failed to create workspace dir: {e}"),
);
}
let mut warnings = Vec::new();
let mut items = 0;
if soul_path.exists() {
match fs::read_to_string(&soul_path) {
Ok(md) => {
let toml_str = markdown_to_personality_toml(&md, "os");
if let Err(e) = fs::write(out_dir.join("OS.toml"), &toml_str) {
return err(
MigrationArea::Personality,
format!("Failed to write OS.toml: {e}"),
);
}
items += 1;
}
Err(e) => {
return err(
MigrationArea::Personality,
format!("Failed to read SOUL.md: {e}"),
);
}
}
} else {
warnings.push("SOUL.md not found; OS.toml will use defaults".into());
}
if agents_path.exists() {
match fs::read_to_string(&agents_path) {
Ok(md) => {
let toml_str = markdown_to_personality_toml(&md, "firmware");
if let Err(e) = fs::write(out_dir.join("FIRMWARE.toml"), &toml_str) {
return err(
MigrationArea::Personality,
format!("Failed to write FIRMWARE.toml: {e}"),
);
}
items += 1;
}
Err(e) => {
return err(
MigrationArea::Personality,
format!("Failed to read AGENTS.md: {e}"),
);
}
}
} else {
warnings.push("AGENTS.md not found; FIRMWARE.toml will use defaults".into());
}
AreaResult {
area: MigrationArea::Personality,
success: true,
items_processed: items,
warnings,
error: None,
}
}
pub(crate) fn export_personality(ic_root: &Path, oc_root: &Path) -> AreaResult {
let ic_ws = ic_root.join("workspace");
let out_ws = oc_root.join("workspace");
if let Err(e) = fs::create_dir_all(&out_ws) {
return err(
MigrationArea::Personality,
format!("Failed to create workspace dir: {e}"),
);
}
let mut warnings = Vec::new();
let mut items = 0;
let os_path = ic_ws.join("OS.toml");
if os_path.exists() {
match fs::read_to_string(&os_path) {
Ok(content) => {
let md = personality_toml_to_markdown(&content, "SOUL");
if let Err(e) = fs::write(out_ws.join("SOUL.md"), &md) {
return err(
MigrationArea::Personality,
format!("Failed to write SOUL.md: {e}"),
);
}
items += 1;
}
Err(e) => {
return err(
MigrationArea::Personality,
format!("Failed to read OS.toml: {e}"),
);
}
}
} else {
warnings.push("OS.toml not found; SOUL.md will be minimal".into());
}
let fw_path = ic_ws.join("FIRMWARE.toml");
if fw_path.exists() {
match fs::read_to_string(&fw_path) {
Ok(content) => {
let md = personality_toml_to_markdown(&content, "AGENTS");
if let Err(e) = fs::write(out_ws.join("AGENTS.md"), &md) {
return err(
MigrationArea::Personality,
format!("Failed to write AGENTS.md: {e}"),
);
}
items += 1;
}
Err(e) => {
return err(
MigrationArea::Personality,
format!("Failed to read FIRMWARE.toml: {e}"),
);
}
}
} else {
warnings.push("FIRMWARE.toml not found; AGENTS.md will be minimal".into());
}
AreaResult {
area: MigrationArea::Personality,
success: true,
items_processed: items,
warnings,
error: None,
}
}
pub(crate) fn markdown_to_personality_toml(md: &str, kind: &str) -> String {
let mut lines = vec![
format!("# Converted from Legacy {kind} markdown"),
format!("[{kind}]"),
];
lines.push(format!("prompt_text = {}", qt_ml(md)));
let mut current_section = String::new();
let mut section_content = Vec::new();
for line in md.lines() {
if line.starts_with("# ") || line.starts_with("## ") {
if !current_section.is_empty() && !section_content.is_empty() {
let key = current_section.to_lowercase().replace([' ', '-'], "_");
let val = section_content.join("\n");
lines.push(format!("{key} = {}", qt_ml(&val)));
section_content.clear();
}
current_section = line.trim_start_matches('#').trim().to_string();
} else if !line.trim().is_empty() {
section_content.push(line.to_string());
}
}
if !current_section.is_empty() && !section_content.is_empty() {
let key = current_section.to_lowercase().replace([' ', '-'], "_");
let val = section_content.join("\n");
lines.push(format!("{key} = {}", qt_ml(&val)));
}
lines.join("\n") + "\n"
}
pub(crate) fn personality_toml_to_markdown(toml_str: &str, title: &str) -> String {
let parsed: Result<toml::Value, _> = toml::from_str(toml_str);
match parsed {
Ok(toml::Value::Table(table)) => {
for (_section_key, section_val) in &table {
if let toml::Value::Table(inner) = section_val
&& let Some(pt) = inner.get("prompt_text").and_then(|v| v.as_str())
&& !pt.is_empty()
{
return pt.to_string();
}
if let Some(pt) = section_val.as_str()
&& _section_key == "prompt_text"
&& !pt.is_empty()
{
return pt.to_string();
}
}
let mut lines = vec![format!("# {title}"), String::new()];
for (_section_key, section_val) in &table {
if let toml::Value::Table(inner) = section_val {
for (key, val) in inner {
if key == "prompt_text" {
continue;
}
let heading = titlecase(key);
lines.push(format!("## {heading}"));
lines.push(String::new());
if let Some(s) = val.as_str() {
lines.push(s.to_string());
} else {
lines.push(val.to_string());
}
lines.push(String::new());
}
}
}
lines.join("\n") + "\n"
}
_ => format!("# {title}\n\n{toml_str}\n"),
}
}
pub(crate) fn titlecase(key: &str) -> String {
key.replace('_', " ")
.split_whitespace()
.map(|w| {
let mut c = w.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().to_string() + c.as_str(),
}
})
.collect::<Vec<_>>()
.join(" ")
}