use std::path::Path;
use serde_json::json;
use crate::output::{emit_no_op, emit_ok, CliError, Ctx};
pub fn run(
ctx: Ctx,
slug: &str,
path: &Path,
force: bool,
) -> Result<(), CliError> {
if !is_valid_slug(slug) {
return Err(CliError::validation(
format!("invalid company slug: '{slug}'"),
Some("use lowercase letters, digits, '-' or '_'; must start with [a-z0-9]".into()),
));
}
let already_exists = path.exists();
if already_exists && !force {
let all_present = ["people", "products", "customers", "chats", "repos"]
.iter()
.all(|n| path.join(format!("{n}.json")).exists());
if all_present {
emit_no_op(
ctx,
&format!("{} already initialized — no changes", path.display()),
);
return Ok(());
}
return Err(CliError::conflict(
format!(
"path already exists and is not a complete liber data dir: {}",
path.display()
),
Some("re-run with --force to overwrite, or point at an empty path".into()),
));
}
std::fs::create_dir_all(path).map_err(|e| {
CliError::data(
format!("could not create {}: {e}", path.display()),
Some("check parent directory permissions".into()),
)
})?;
write_file(path, "people.json", &people_template(slug), force)?;
write_file(path, "products.json", &products_template(slug), force)?;
write_file(path, "customers.json", &customers_template(), force)?;
write_file(path, "chats.json", &chats_template(), force)?;
write_file(path, "repos.json", &repos_template(slug), force)?;
let payload = json!({
"ok": true,
"company": slug,
"data_dir": path.display().to_string(),
"files": ["people.json", "products.json", "customers.json", "chats.json", "repos.json"],
"next": [
format!("cd {}", path.display()),
"edit people.json / products.json / customers.json / chats.json".to_string(),
format!("LIBER_DATA_DIR=. liber validate"),
format!("LIBER_DATA_DIR=. liber people list"),
],
});
if ctx.json {
emit_ok(ctx, payload);
} else {
emit_ok(
ctx,
json!({
"message": format!(
"initialized liber data dir for '{slug}' at {}\nnext: edit the JSON files, then run `LIBER_DATA_DIR=. liber validate`",
path.display()
)
}),
);
}
Ok(())
}
fn is_valid_slug(s: &str) -> bool {
if s.is_empty() {
return false;
}
let first = s.chars().next().unwrap();
if !(first.is_ascii_lowercase() || first.is_ascii_digit()) {
return false;
}
s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
}
fn write_file(dir: &Path, name: &str, value: &serde_json::Value, force: bool) -> Result<(), CliError> {
let target = dir.join(name);
if target.exists() && !force {
return Ok(());
}
let bytes = serde_json::to_vec_pretty(value).expect("serialize template");
std::fs::write(&target, bytes).map_err(|e| {
CliError::data(
format!("could not write {}: {e}", target.display()),
None,
)
})?;
Ok(())
}
fn people_template(_slug: &str) -> serde_json::Value {
json!({
"departments": {
"Engineering": {
"members": [
{
"name": "Ada Example",
"email": "ada@example.com",
"github": "ada-example",
"git_aliases": ["ada", "Ada Example"]
}
]
}
},
"root_users": [],
"updated_at": today()
})
}
fn products_template(slug: &str) -> serde_json::Value {
json!({
"products": [
{
"slug": format!("{slug}-core"),
"name": "Example Product",
"description": "Main product (edit me)."
}
],
"updated_at": today()
})
}
fn customers_template() -> serde_json::Value {
json!({
"customers": [
{
"slug": "example-customer",
"name": "Example Customer",
"related_products": [],
"chats": [],
"notes": null
}
],
"updated_at": today()
})
}
fn chats_template() -> serde_json::Value {
json!({
"platform": "feishu",
"group_chats": {},
"updated_at": today()
})
}
fn repos_template(slug: &str) -> serde_json::Value {
json!({
"repos": [
{
"slug": format!("{slug}-core"),
"url": format!("https://github.com/{slug}/{slug}-core"),
"visibility": "private",
"description": "Main repo (edit me)."
}
],
"updated_at": today()
})
}
fn today() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let days = secs / 86_400;
let (y, m, d) = civil_from_days(days);
format!("{y:04}-{m:02}-{d:02}")
}
fn civil_from_days(z: i64) -> (i32, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u32;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as i32, m, d)
}