use std::path::Path;
use anyhow::Result;
use crate::{doctor, hooks, install, pipeline};
pub fn run_workspace(path: &Path, name: &str, members: &[String]) -> Result<bool> {
if path.exists() {
anyhow::bail!("{} already exists — refusing to overwrite", path.display());
}
let mut rows = String::new();
let mut width = 0usize;
let mut parsed: Vec<(String, String)> = Vec::new();
for member in members {
let (mname, mpath) = match member.split_once('=') {
Some((n, p)) => (n.trim().to_string(), p.trim().to_string()),
None => {
let p = member.trim();
let n = Path::new(p)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.ok_or_else(|| anyhow::anyhow!("cannot derive a member name from {p:?}"))?;
(n, p.to_string())
}
};
anyhow::ensure!(!mname.is_empty(), "empty member name in {member:?}");
let expanded = if let Some(rest) = mpath.strip_prefix("~/") {
std::env::var_os("HOME")
.map(|h| Path::new(&h).join(rest))
.ok_or_else(|| anyhow::anyhow!("cannot expand ~ in {mpath:?}"))?
} else {
Path::new(&mpath).to_path_buf()
};
anyhow::ensure!(
expanded.is_dir(),
"member {mname}: {mpath} is not a directory"
);
anyhow::ensure!(
parsed.iter().all(|(n, _)| n != &mname),
"duplicate member name {mname:?}"
);
width = width.max(mname.len());
parsed.push((mname, mpath));
}
for (n, p) in &parsed {
rows.push_str(&format!("{n:width$} = \"{p}\"\n"));
}
let members_block = if rows.is_empty() {
"# auth = \"~/src/auth\"\n# billing = \"~/src/billing\"".to_string()
} else {
rows.trim_end().to_string()
};
let template = format!(
r#"# sinter workspace manifest — see `sinter workspace --help`
[workspace]
name = "{name}"
# Repos forming the system. Key = member name (used as the symbol
# prefix, e.g. `auth:Login`), value = repo path (~ expands).
[members]
{members_block}
# Optional: runtime coupling no parser can see (queue topics, RPC by
# config). Each entry becomes an edge with `declared` evidence; a
# missing or ambiguous symbol fails `sinter workspace` loudly.
# [[links]]
# from_member = "billing"
# from_symbol = "ConsumeSettled"
# to_member = "auth"
# to_symbol = "PublishSettled"
# via = "topic payments.settled"
"#
);
std::fs::write(path, template)?;
println!("wrote {}", path.display());
if members.is_empty() {
println!(
"next: fill in [members], then run `sinter workspace {}`",
path.display()
);
} else {
println!("next: sinter workspace {}", path.display());
}
Ok(true)
}
pub fn run(repo: &Path, cursor: bool, scip: Option<bool>, global: bool) -> Result<bool> {
let repo = repo.canonicalize()?;
let cursor = cursor || repo.join(".cursor").exists();
println!("== build ==");
let report = pipeline::build(&repo, None)?;
pipeline::print_report(&report);
println!("\n== scip (compiler evidence) ==");
let consent = scip.unwrap_or_else(|| {
use std::io::IsTerminal;
if std::io::stdin().is_terminal() {
print!(
"run compiler indexers? They build the project and can execute \
repository build scripts [y/N] "
);
use std::io::Write;
let _ = std::io::stdout().flush();
let mut answer = String::new();
let _ = std::io::stdin().read_line(&mut answer);
matches!(answer.trim(), "y" | "Y" | "yes")
} else {
println!("skipped: non-interactive and no --scip (indexers execute build scripts)");
false
}
});
if consent {
if let Err(e) = crate::scip::run(&repo) {
println!("skipped: {e:#}");
println!("(optional — rerun `sinter scip` once an indexer is installed)");
}
} else {
println!("(optional — run `sinter scip` when you trust this repository)");
}
println!("\n== git hooks ==");
if repo.join(".git").exists() {
hooks::install(&repo)?;
} else {
println!("not a git repository — skipping hooks");
}
println!("\n== agent integration ==");
let mut targets = vec![
"claude".to_string(),
"agents".to_string(),
"enforce".to_string(),
];
if cursor {
targets.push("cursor".to_string());
}
install::run_targets(&targets, None, true, &repo, false, false)?;
if global {
install::enforce(None, false)?;
}
pipeline::build(&repo, None)?;
println!("\ntip: `sinter completion <shell>` prints shell completions (bash/zsh/fish)");
println!("\n== doctor ==");
doctor::run(&repo, false)
}