Add fabro repo deinit command to reverse project initialization

Removes fabro.toml and the fabro/ directory from the git repo root.
Fails with a clear error when the project is not initialized.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-18 17:51:45 -04:00
parent a41bc899e4
commit 7c16f3b3c9
No known key found for this signature in database
3 changed files with 118 additions and 0 deletions

View file

@ -110,6 +110,59 @@ draft = true
Ok(())
}
pub fn run_deinit() -> Result<()> {
let output = std::process::Command::new("git")
.args(["rev-parse", "--show-toplevel"])
.output()
.context("failed to run git")?;
if !output.status.success() {
bail!("not a git repository");
}
let repo_root = PathBuf::from(
String::from_utf8(output.stdout)
.context("git output was not valid UTF-8")?
.trim(),
);
let fabro_toml = repo_root.join("fabro.toml");
if !fabro_toml.exists() {
bail!("not initialized — fabro.toml not found");
}
let green = console::Style::new().green();
let dim = console::Style::new().dim();
std::fs::remove_file(&fabro_toml)
.with_context(|| format!("failed to remove {}", fabro_toml.display()))?;
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("removed fabro.toml")
);
let fabro_dir = repo_root.join("fabro");
if fabro_dir.exists() {
std::fs::remove_dir_all(&fabro_dir)
.with_context(|| format!("failed to remove {}", fabro_dir.display()))?;
eprintln!(
" {} {}",
green.apply_to(""),
dim.apply_to("removed fabro/")
);
}
eprintln!(
"\n{}",
console::Style::new()
.bold()
.apply_to("Project deinitialized.")
);
Ok(())
}
async fn check_github_app_installation() {
// Get the git remote origin URL
let output = match std::process::Command::new("git")

View file

@ -130,6 +130,7 @@ enum Command {
command: PrCommand,
},
/// Skill management
#[command(hide = true)]
Skill {
#[command(subcommand)]
command: SkillCommand,
@ -199,6 +200,8 @@ enum SystemCommand {
enum RepoCommand {
/// Initialize a new project
Init,
/// Remove fabro.toml and fabro/ directory
Deinit,
}
#[derive(Subcommand)]
@ -450,6 +453,7 @@ async fn main_inner() -> (String, Result<()>) {
Command::Doctor { .. } => "doctor",
Command::Repo { command } => match command {
RepoCommand::Init => "repo init",
RepoCommand::Deinit => "repo deinit",
},
Command::Init => "init",
Command::Install => "install",
@ -783,6 +787,9 @@ async fn main_inner() -> (String, Result<()>) {
RepoCommand::Init => {
init::run_init().await?;
}
RepoCommand::Deinit => {
init::run_deinit()?;
}
},
Command::Init => {
eprintln!(

View file

@ -410,6 +410,64 @@ fn scenario_full_stack(sandbox: &str) {
);
}
// ---------------------------------------------------------------------------
// repo deinit
// ---------------------------------------------------------------------------
fn init_git_repo(path: &Path) {
std::process::Command::new("git")
.args(["init"])
.current_dir(path)
.output()
.expect("git init should succeed");
}
fn init_fabro_project(path: &Path) {
std::fs::write(path.join("fabro.toml"), "version = 1\n").unwrap();
let workflow_dir = path.join("fabro/workflows/hello");
std::fs::create_dir_all(&workflow_dir).unwrap();
std::fs::write(workflow_dir.join("workflow.fabro"), "digraph {}").unwrap();
std::fs::write(workflow_dir.join("workflow.toml"), "version = 1\n").unwrap();
}
#[test]
fn test_repo_deinit_removes_fabro_toml_and_dir() {
let tmp = tempfile::tempdir().unwrap();
init_git_repo(tmp.path());
init_fabro_project(tmp.path());
assert!(tmp.path().join("fabro.toml").exists());
assert!(tmp.path().join("fabro").exists());
fabro()
.args(["repo", "deinit"])
.current_dir(tmp.path())
.assert()
.success();
assert!(
!tmp.path().join("fabro.toml").exists(),
"fabro.toml should be removed"
);
assert!(
!tmp.path().join("fabro").exists(),
"fabro/ directory should be removed"
);
}
#[test]
fn test_repo_deinit_fails_when_not_initialized() {
let tmp = tempfile::tempdir().unwrap();
init_git_repo(tmp.path());
fabro()
.args(["repo", "deinit"])
.current_dir(tmp.path())
.assert()
.failure()
.stderr(predicates::str::contains("not initialized"));
}
// ---------------------------------------------------------------------------
// Standalone tests (no sandbox parametrization)
// ---------------------------------------------------------------------------