pub mod ask_cmd;
pub mod audit_cmd;
pub mod bundle_cmd;
pub mod doctor_cmd;
pub mod down;
pub mod flow_cmd;
pub mod import_cmd;
pub mod keel_cmd;
pub mod logs;
pub mod new_cmd;
pub mod next_cmd;
pub mod pull_cmd;
pub mod recipe;
pub mod run_cmd;
pub mod secrets_status;
pub mod selftest;
pub mod shell;
pub mod ship_cmd;
pub mod spec_cmd;
pub mod status;
pub mod studio_cmd;
pub mod ui_cmd;
pub mod up;
pub mod use_cmd;
pub mod view;
use crate::{compose, manifest::Manifest, paths, proc};
use anyhow::{Context, Result};
use std::io::{self, Write};
use std::path::Path;
pub(crate) fn host_github_token() -> Option<String> {
let (status, out) = proc::run_capture("gh", &["auth", "token"]).ok()?;
let token = out.trim().to_string();
(status.success() && !token.is_empty()).then_some(token)
}
pub(crate) fn run_git_as_host(token: &str, argv: &[&str]) -> Result<(bool, String)> {
std::env::set_var("MOOR_HOST_GH_TOKEN", token);
let helper = "credential.helper=!f() { echo username=x-access-token; echo password=$MOOR_HOST_GH_TOKEN; }; f";
let mut args: Vec<&str> = vec!["-c", helper];
args.extend_from_slice(argv);
let result = proc::run_capture_combined("git", &args);
std::env::remove_var("MOOR_HOST_GH_TOKEN");
let (status, out) = result?;
Ok((status.success(), out))
}
pub(crate) fn stream_into_sandbox(container: &str, bundle_path: &Path, dest: &str) -> Result<()> {
let cmd = format!("cat > {dest}");
let status = proc::run_with_stdin_file(
"docker",
&["exec", "-i", container, "sh", "-c", &cmd],
bundle_path,
)?;
proc::require_success("streaming bundle into the sandbox", status)
}
pub(crate) fn running_projects(names: &[String]) -> Result<Vec<String>> {
let (_status, out) = crate::proc::run_capture("docker", &["ps", "--format", "{{.Names}}"])?;
let running: std::collections::HashSet<&str> = out.lines().collect();
let mut matches = vec![];
for name in names {
let Ok(manifest_path) = paths::manifest_path(name) else {
continue;
};
if !manifest_path.exists() {
continue;
}
if let Ok(m) = Manifest::load(&manifest_path) {
if running.contains(m.sandbox_container().as_str()) {
matches.push(name.clone());
}
}
}
Ok(matches)
}
#[derive(Debug)]
pub enum ProjectSource {
Sticky,
RunningUnique,
SoleProject,
}
impl ProjectSource {
fn describe(&self) -> &'static str {
match self {
ProjectSource::Sticky => "set via `moor use`",
ProjectSource::RunningUnique => "only sandbox currently up",
ProjectSource::SoleProject => "only project you have",
}
}
}
fn pick_running_or_sole_project(
all: &[String],
running: &[String],
) -> Result<(String, ProjectSource)> {
if running.len() == 1 {
return Ok((running[0].clone(), ProjectSource::RunningUnique));
}
match all.len() {
0 => anyhow::bail!("no projects yet — try `moor new <name>`"),
1 => Ok((all[0].clone(), ProjectSource::SoleProject)),
_ if running.len() > 1 => anyhow::bail!(
"more than one project is up ({}) — pass `--project <name>` or run `moor use <name>`",
running.join(", ")
),
_ => anyhow::bail!(
"more than one project exists ({}) and none is up — run `moor up <name>`, `moor use <name>`, or pass `--project <name>`",
all.join(", ")
),
}
}
pub fn resolve_project(explicit: Option<String>) -> Result<(String, Option<ProjectSource>)> {
if let Some(name) = explicit {
return Ok((name, None));
}
if let Some(name) = paths::read_current_project()? {
return Ok((name, Some(ProjectSource::Sticky)));
}
let all = paths::all_project_names()?;
let running = running_projects(&all)?;
let (name, source) = pick_running_or_sole_project(&all, &running)?;
Ok((name, Some(source)))
}
pub fn resolve_project_announced(explicit: Option<String>) -> Result<String> {
let (name, source) = resolve_project(explicit)?;
match source {
Some(source) => println!("==> project: {name} ({})", source.describe()),
None => println!("==> project: {name}"),
}
Ok(name)
}
pub fn create_github_repo_interactive(name: &str) -> Result<Option<String>> {
print!(
"About to run `gh repo create {name} --private` on your GitHub account. Continue? [y/N] "
);
io::stdout().flush().ok();
let mut answer = String::new();
io::stdin().read_line(&mut answer)?;
if !answer.trim().eq_ignore_ascii_case("y") {
return Ok(None);
}
let (status, whoami) = crate::proc::run_capture("gh", &["api", "user", "-q", ".login"])?;
crate::proc::require_success("gh api user", status)?;
let owner = whoami.trim();
let repo_slug = format!("{owner}/{name}");
let status = crate::proc::run_inherit("gh", &["repo", "create", &repo_slug, "--private"])?;
crate::proc::require_success("gh repo create", status)?;
Ok(Some(repo_slug))
}
pub fn compose_up(name: &str) -> Result<()> {
let m = Manifest::load(&paths::manifest_path(name)?)?;
crate::secrets::resolve_into_env(name, &m.secrets);
let rendered = compose::render(&m);
let compose_path = paths::compose_path(name)?;
std::fs::write(&compose_path, rendered)
.with_context(|| format!("writing {}", compose_path.display()))?;
let compose_path_str = compose_path.to_string_lossy().to_string();
let status = crate::proc::run_inherit(
"docker",
&["compose", "-p", name, "-f", &compose_path_str, "up", "-d"],
)?;
crate::proc::require_success("docker compose up", status)?;
sync_git_identity(&m);
if let Err(e) = crate::posture::attest(name, &m) {
eprintln!("warning: could not attest the sandbox's posture: {e:#}");
}
Ok(())
}
pub fn sync_git_identity(m: &Manifest) {
let container = m.sandbox_container();
for key in ["user.name", "user.email"] {
if let Ok((status, out)) = crate::proc::run_capture("git", &["config", "--get", key]) {
let value = out.trim();
if status.success() && !value.is_empty() {
let _ = crate::proc::run_capture(
"docker",
&["exec", &container, "git", "config", key, value],
);
}
}
}
}
pub fn compose_down(name: &str) -> Result<()> {
let compose_path = paths::compose_path(name)?;
if !compose_path.exists() {
anyhow::bail!("no compose.yml for project '{name}' — has `moor new` been run?");
}
let compose_path_str = compose_path.to_string_lossy().to_string();
let status = crate::proc::run_inherit(
"docker",
&["compose", "-p", name, "-f", &compose_path_str, "down"],
)?;
crate::proc::require_success("docker compose down", status)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn names(items: &[&str]) -> Vec<String> {
items.iter().map(|s| s.to_string()).collect()
}
#[test]
fn one_running_project_wins_even_with_others_present() {
let all = names(&["banner-demo", "isolator-sample-app", "keel"]);
let running = names(&["keel"]);
let (name, source) = pick_running_or_sole_project(&all, &running).unwrap();
assert_eq!(name, "keel");
assert!(matches!(source, ProjectSource::RunningUnique));
}
#[test]
fn sole_project_wins_when_none_are_running() {
let all = names(&["only-project"]);
let running = names(&[]);
let (name, source) = pick_running_or_sole_project(&all, &running).unwrap();
assert_eq!(name, "only-project");
assert!(matches!(source, ProjectSource::SoleProject));
}
#[test]
fn no_projects_at_all_is_an_error() {
let err = pick_running_or_sole_project(&[], &[]).unwrap_err();
assert!(err.to_string().contains("moor new"));
}
#[test]
fn multiple_running_projects_is_ambiguous() {
let all = names(&["banner-demo", "keel"]);
let running = names(&["banner-demo", "keel"]);
let err = pick_running_or_sole_project(&all, &running).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("banner-demo") && msg.contains("keel"));
assert!(msg.contains("--project") || msg.contains("moor use"));
}
#[test]
fn multiple_projects_none_running_is_ambiguous() {
let all = names(&["banner-demo", "isolator-sample-app", "keel"]);
let err = pick_running_or_sole_project(&all, &[]).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("none is up"));
assert!(msg.contains("moor up"));
}
#[test]
fn describes_each_source_distinctly() {
let sticky = ProjectSource::Sticky.describe();
let running = ProjectSource::RunningUnique.describe();
let sole = ProjectSource::SoleProject.describe();
assert_ne!(sticky, running);
assert_ne!(running, sole);
assert_ne!(sticky, sole);
}
}