feat(cli/org): wire Commands::Org admin subcommands + parse_org_role + transfer-ownership/delete-org

This commit is contained in:
adlee-was-taken
2026-06-20 13:50:11 -04:00
parent cdb008c900
commit 6a16523ee0
2 changed files with 197 additions and 2 deletions

View File

@@ -441,6 +441,73 @@ pub fn run_rotate_key(dir: &Path) -> Result<()> {
Ok(())
}
pub fn run_transfer_ownership(dir: &Path, member_id_prefix: &str, keep_owner: bool) -> Result<()> {
let vault = crate::org_session::open_org_vault(Some(dir))?;
let caller = vault.current_member()?;
if !caller.role.can_manage_owners() {
anyhow::bail!("only an owner can transfer ownership");
}
let mut members = vault.load_members()?;
let target_id = resolve_member_id(&members, member_id_prefix)?;
if target_id == caller.member_id {
anyhow::bail!("you are already the owner");
}
// Promote the target to Owner.
{
let target = members.find_by_id_mut(&target_id)
.ok_or_else(|| anyhow::anyhow!("member not found"))?;
target.role = OrgRole::Owner;
}
// Real transfer: also demote the CALLER to Admin, unless --keep-owner was
// passed (explicit co-ownership). The spec says "owner → another member",
// so demotion is the default.
if !keep_owner {
if let Some(me) = members.find_by_id_mut(&caller.member_id) {
me.role = OrgRole::Admin;
}
}
vault.save_members(&members)?;
let mode = if keep_owner { "co-ownership (caller kept owner)" } else { "caller demoted to admin" };
let commit_msg = format!(
"org: transfer ownership to {} ({mode})\n\nRelicario-Actor: {} {}\nRelicario-Action: ownership-transfer\nRelicario-Member: {}",
target_id.as_str(), caller.display_name, caller.member_id.as_str(), target_id.as_str()
);
crate::org_session::org_git_run(&vault.root, &["add", "members.json"], "git add")?;
crate::org_session::org_git_run(&vault.root, &["commit", "-m", &commit_msg], "git commit")?;
if keep_owner {
println!("Ownership shared with {} (you remain an owner).", target_id.as_str());
} else {
println!("Ownership transferred to {} (you are now an admin).", target_id.as_str());
}
Ok(())
}
pub fn run_delete_org(dir: &Path, confirm: bool) -> Result<()> {
let vault = crate::org_session::open_org_vault(Some(dir))?;
let caller = vault.current_member()?;
if !caller.role.can_manage_owners() {
anyhow::bail!("only an owner can delete the org");
}
if !confirm {
anyhow::bail!("refusing to delete org without --confirm");
}
let commit_msg = format!(
"org: delete org\n\nRelicario-Actor: {} {}\nRelicario-Action: org-delete",
caller.display_name, caller.member_id.as_str()
);
// Remove org files (the git history is retained as the audit record).
for f in ["org.json", "members.json", "collections.json", "manifest.enc"] {
let _ = fs::remove_file(vault.root.join(f));
}
let _ = std::fs::remove_dir_all(vault.root.join("items"));
let _ = std::fs::remove_dir_all(vault.root.join("keys"));
crate::org_session::org_git_run(&vault.root, &["add", "-A"], "git add")?;
crate::org_session::org_git_run(&vault.root, &["commit", "-m", &commit_msg], "git commit")?;
println!("Org deleted (git history retained as audit record).");
Ok(())
}
pub fn run_status(dir: &Path) -> Result<()> {
let root = crate::org_session::org_dir(Some(dir))?;