use crate::{audit, manifest, manifest::Manifest, paths, proc};
use anyhow::{Context, Result};
use std::path::Path;
fn detect_image(from: &Path) -> &'static str {
if from.join("Cargo.toml").exists() {
"moor/rust:latest"
} else if from.join("package.json").exists() {
"moor/node:latest"
} else if from.join("pyproject.toml").exists() || from.join("requirements.txt").exists() {
"moor/python:latest"
} else {
"moor/base:latest"
}
}
fn parse_github_repo_slug(url: &str) -> Option<String> {
let url = url.trim().trim_end_matches(".git");
let rest = url
.strip_prefix("https://github.com/")
.or_else(|| url.strip_prefix("http://github.com/"))
.or_else(|| url.strip_prefix("ssh://git@github.com/"))
.or_else(|| url.strip_prefix("git@github.com:"))?;
let (owner, repo) = rest.split_once('/')?;
if owner.is_empty() || repo.is_empty() || repo.contains('/') {
return None;
}
Some(format!("{owner}/{repo}"))
}
pub fn run(
name: &str,
from: &Path,
image: Option<String>,
agent: manifest::Agent,
github: bool,
) -> Result<()> {
manifest::validate_name(name)?;
let dir = paths::project_dir(name)?;
if dir.exists() {
anyhow::bail!("project '{name}' already exists at {}", dir.display());
}
let from = from
.canonicalize()
.with_context(|| format!("'{}' does not exist", from.display()))?;
if !from.join(".git").exists() {
anyhow::bail!(
"'{}' is not a git repository (no .git) — moor only imports version-controlled projects",
from.display()
);
}
let language_image = image.unwrap_or_else(|| detect_image(&from).to_string());
let image = manifest::resolve_image(&language_image, agent);
println!(
"==> importing {} as '{name}' (image: {image}, agent: {agent:?})",
from.display()
);
let mut m = Manifest::new(name, &image);
m.agent = agent;
let (status, out) = proc::run_capture(
"git",
&["-C", &from.to_string_lossy(), "remote", "get-url", "origin"],
)?;
let existing_remote = if status.success() {
Some(out.trim().to_string())
} else {
None
};
if let Some(url) = &existing_remote {
if let Some(slug) = parse_github_repo_slug(url) {
m.github_repo = Some(slug);
}
} else if github {
match super::create_github_repo_interactive(name)? {
Some(slug) => m.github_repo = Some(slug),
None => {
println!("Aborted — no GitHub repo created, no project scaffolded.");
return Ok(());
}
}
}
paths::ensure_project_dirs(name)?;
m.save(&paths::manifest_path(name)?)
.context("writing moor.yaml")?;
println!("==> starting sandbox + egress for '{name}'");
super::compose_up(name)?;
import_history(name, &m, &from, existing_remote.as_deref())?;
super::sync_git_identity(&m);
println!("==> checking whether the workspace is already set up for specs");
let (status, _) = proc::run_capture(
"docker",
&[
"exec",
&m.sandbox_container(),
"test",
"-f",
".keel/keel.toml",
],
)?;
if status.success() {
println!(" already set up — leaving it as-is (`moor next` shows where its specs stand)");
} else {
println!(" not yet — setting it up inside the sandbox");
let status =
proc::run_capture_combined("docker", &["exec", &m.sandbox_container(), "keel", "init"]);
match status {
Ok((s, out)) => {
audit::log_exec(name, &m, "exec", &["keel".into(), "init".into()], s.code())?;
if !s.success() {
println!("{out}");
println!(" note: workspace setup did not exit cleanly — check with `moor shell {name}`");
}
}
Err(e) => println!(" note: could not set the workspace up automatically: {e}"),
}
}
println!(
"\n'{name}' is up, imported from {}. Manifest: {}\n\n{}",
from.display(),
paths::manifest_path(name)?.display(),
super::next_cmd::next_hint(name)
);
Ok(())
}
fn import_history(name: &str, m: &Manifest, from: &Path, real_remote: Option<&str>) -> Result<()> {
let bundle_path = paths::project_dir(name)?.join(".import.bundle");
let bundle_str = bundle_path.to_string_lossy().to_string();
let from_str = from.to_string_lossy().to_string();
println!("==> bundling {} (all branches and tags)", from.display());
let status = proc::run_inherit(
"git",
&["-C", &from_str, "bundle", "create", &bundle_str, "--all"],
)?;
proc::require_success("git bundle create", status)?;
let container = m.sandbox_container();
println!("==> streaming the bundle into the sandbox and cloning it");
let status = proc::run_with_stdin_file(
"docker",
&[
"exec",
"-i",
&container,
"sh",
"-c",
"cat > /tmp/import.bundle",
],
&bundle_path,
)?;
proc::require_success("streaming import bundle into sandbox", status)?;
let _ = std::fs::remove_file(&bundle_path);
let status = proc::run_inherit(
"docker",
&[
"exec",
&container,
"git",
"clone",
"/tmp/import.bundle",
".",
],
)?;
audit::log_exec(
name,
m,
"exec",
&["git".into(), "clone".into(), "<imported bundle>".into()],
status.code(),
)?;
proc::require_success("git clone (imported bundle) in sandbox", status)?;
let _ = proc::run_capture(
"docker",
&["exec", &container, "rm", "-f", "/tmp/import.bundle"],
);
let materialize = "for b in $(git for-each-ref --format='%(refname:short)' refs/remotes/origin/ | grep -v '/HEAD$'); do name=${b#origin/}; git show-ref --verify --quiet \"refs/heads/$name\" || git branch \"$name\" \"$b\"; done";
let status = proc::run_inherit("docker", &["exec", &container, "sh", "-c", materialize])?;
proc::require_success("materializing imported branches locally", status)?;
if let Some(url) = real_remote {
let status = proc::run_inherit(
"docker",
&[
"exec", &container, "git", "remote", "set-url", "origin", url,
],
)?;
proc::require_success("git remote set-url origin", status)?;
println!(" origin set to {url}");
} else if let Some(slug) = &m.github_repo {
let url = format!("https://github.com/{slug}.git");
let status = proc::run_inherit(
"docker",
&[
"exec", &container, "git", "remote", "set-url", "origin", &url,
],
)?;
proc::require_success("git remote set-url origin", status)?;
println!(" origin set to {url} (created by --github)");
} else {
let _ = proc::run_capture(
"docker",
&["exec", &container, "git", "remote", "remove", "origin"],
);
println!(" no remote configured — set one with `moor run {name} -- git remote add origin <url>` when ready");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn touch(dir: &Path, file: &str) {
std::fs::write(dir.join(file), "").unwrap();
}
fn temp_dir(label: &str) -> PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("moor-import-test-{label}-{nanos}"));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn detects_rust_project() {
let dir = temp_dir("rust");
touch(&dir, "Cargo.toml");
assert_eq!(detect_image(&dir), "moor/rust:latest");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn detects_node_project() {
let dir = temp_dir("node");
touch(&dir, "package.json");
assert_eq!(detect_image(&dir), "moor/node:latest");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn detects_python_project_via_pyproject() {
let dir = temp_dir("py-pyproject");
touch(&dir, "pyproject.toml");
assert_eq!(detect_image(&dir), "moor/python:latest");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn detects_python_project_via_requirements_txt() {
let dir = temp_dir("py-reqs");
touch(&dir, "requirements.txt");
assert_eq!(detect_image(&dir), "moor/python:latest");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn falls_back_to_base_when_nothing_recognized() {
let dir = temp_dir("empty");
assert_eq!(detect_image(&dir), "moor/base:latest");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn rust_takes_priority_when_multiple_markers_present() {
let dir = temp_dir("mixed");
touch(&dir, "Cargo.toml");
touch(&dir, "package.json");
assert_eq!(detect_image(&dir), "moor/rust:latest");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn parses_https_github_url() {
assert_eq!(
parse_github_repo_slug("https://github.com/daneb/keel"),
Some("daneb/keel".to_string())
);
assert_eq!(
parse_github_repo_slug("https://github.com/daneb/keel.git"),
Some("daneb/keel".to_string())
);
}
#[test]
fn parses_ssh_github_url_forms() {
assert_eq!(
parse_github_repo_slug("git@github.com:daneb/keel.git"),
Some("daneb/keel".to_string())
);
assert_eq!(
parse_github_repo_slug("ssh://git@github.com/daneb/keel.git"),
Some("daneb/keel".to_string())
);
}
#[test]
fn returns_none_for_non_github_remotes() {
assert_eq!(
parse_github_repo_slug("https://gitlab.com/daneb/keel.git"),
None
);
assert_eq!(parse_github_repo_slug("/Users/danebalia/Repos/keel"), None);
assert_eq!(parse_github_repo_slug(""), None);
}
}