use std::path::PathBuf;
use anyhow::{Context, bail};
use semora_proto::{CreateProject, Level};
use crate::args::ProjectCmd;
use crate::client::ProjectCreated;
use crate::{Ctx, exit, output, project_link};
pub async fn run(ctx: &Ctx, cmd: ProjectCmd) -> anyhow::Result<i32> {
match cmd {
ProjectCmd::Init { dir, name, join } => init(ctx, dir, name, join).await,
ProjectCmd::Ls => ls(ctx).await,
ProjectCmd::Map(a) => super::map::run(ctx, a).await,
}
}
async fn init(
ctx: &Ctx,
dir: Option<PathBuf>,
name: Option<String>,
join: bool,
) -> anyhow::Result<i32> {
let client = ctx.client()?;
let dir = std::fs::canonicalize(dir.unwrap_or_else(|| PathBuf::from(".")))
.context("cannot read the project folder")?;
if !dir.is_dir() {
bail!("{} is not a folder", project_link::shown(&dir));
}
let dir_name = dir
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
if let Some(n) = &name
&& !project_link::valid_slug(n)
{
bail!(
"`{n}` is not a valid project id: use lowercase letters, digits and dashes, up to 40 characters, starting with a letter or digit"
);
}
let server = client.origin();
let existing = project_link::read_at(&dir)?;
if let Some(l) = &existing
&& (l.server != server || name.as_ref().is_some_and(|n| *n != l.slug))
{
bail!(
"{} already links this folder to project `{}` on {}. Remove it to start again.",
project_link::shown(&dir.join(project_link::FILE)),
l.slug,
l.server
);
}
let slug = match (name, &existing) {
(Some(n), _) => n,
(None, Some(l)) => l.slug.clone(),
(None, None) => project_link::slug_from(&dir_name).with_context(|| {
format!("can't make a project id from `{dir_name}`; pass one with --name")
})?,
};
let req = CreateProject {
slug: slug.clone(),
name: Some(dir_name).filter(|n| !n.trim().is_empty()),
join,
};
let (info, created) = match client.create_project(&req).await? {
ProjectCreated::Created(p) => (p, true),
ProjectCreated::Linked(p) => (p, false),
ProjectCreated::Taken => bail!(
"the project id `{slug}` belongs to someone else; choose another with --name, for example `--name {slug}-2`"
),
ProjectCreated::JoinNeeded { owner, level } => bail!(
"Project `{slug}` belongs to {}, and you are {} {} in it. Run `semora project init --join` to publish into it, or choose another id with --name.",
output::clean_name(&owner),
if level == Level::Editor { "an" } else { "a" },
level.label()
),
};
let wrote = if existing.is_none() {
Some(project_link::write(&dir, &server, &slug)?)
} else {
None
};
if ctx.json {
output::json(&info);
return Ok(exit::OK);
}
let done = output::out().done;
if created {
anstream::println!("{done}Created{done:#} project \"{}\" ({slug})", info.name);
} else if info.level == Level::Owner {
anstream::println!(
"{done}Linked{done:#} to your existing project \"{}\" ({slug})",
info.name
);
} else {
anstream::println!(
"{done}Joined{done:#} project \"{}\" ({slug}) as {} {}",
info.name,
if info.level == Level::Editor {
"an"
} else {
"a"
},
info.level.label()
);
}
if let Some(file) = wrote {
println!("Wrote {}", project_link::shown(&file));
println!("Commit it so every checkout publishes to the same project.");
}
anstream::println!("{LINK}{}{LINK:#}", info.url, LINK = output::LINK);
Ok(exit::OK)
}
async fn ls(ctx: &Ctx) -> anyhow::Result<i32> {
let client = ctx.client()?;
let projects = client.projects().await?;
if ctx.json {
output::json(&projects);
return Ok(exit::OK);
}
if projects.is_empty() {
println!("No projects. Run `semora project init` in a folder.");
return Ok(exit::OK);
}
for p in &projects {
println!(
"{:<24} {:<9} {:>4} docs {} {}",
p.slug,
p.level.as_str(),
p.documents,
output::ts(p.updated_at),
p.name
);
}
Ok(exit::OK)
}