use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use aristo_core::config::ConfigFile;
use aristo_core::index::{IndexFile, Meta};
use crate::{CliError, CliResult};
const PRE_COMMIT_HOOK: &str = "\
#!/usr/bin/env bash
# Aristo pre-commit hook. Installed by `aristo init`.
# Refreshes .aristo/index.toml + .aristo/doc/<id>.md files, then runs
# lint --check on the index. Non-zero exit aborts the commit.
set -e
echo \"-> aristo stamp\" >&2
aristo stamp >&2
echo \"-> aristo doc\" >&2
aristo doc >&2
echo \"-> aristo lint --check\" >&2
aristo lint --check >&2
";
const GH_WORKFLOW_STARTER: &str = "\
# Aristo CI gates (starter; edit freely).
# Generated by `aristo init`.
#
# Gate order: stamp first (everything downstream reads the index — fail
# fast on staleness), lint second (cheapest static check), doc third
# (purely derived from index). Each `--check` mode is non-mutating and
# exits non-zero on drift.
#
# NOTE: the `verify --check --strict` step is intentionally omitted while
# the `verify=\"test\"` and `verify=\"full\"` pipelines are still under
# design. Once `verify=\"test\"` ships, add this step BEFORE `doc --check`:
# - name: verify --check --strict (no stale/orphan/forged)
# run: aristo verify --check --strict
#
# The badge step generates a fresh SVG on every push and uploads it as
# a workflow artifact. The artifact URL is per-build (GH-Actions
# specific); for a stable README badge, either commit `docs/badge.svg`
# directly (regenerated locally via the pre-commit hook) or set up a
# gh-pages publish step downstream of this workflow.
name: aristo
on: [push, pull_request]
jobs:
aristo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: cargo install aristo --locked
- name: stamp --check (index in sync with source)
run: aristo stamp --check
- name: lint --check --strict (no warn/error findings)
run: aristo lint --check --strict
- name: doc --check (doc artifacts in sync with index)
run: aristo doc --check
- name: status (per-pipeline rate + tier — informational)
run: aristo status
- name: badge (regenerate SVG)
run: aristo badge --out=docs/badge.svg
- name: upload badge artifact
uses: actions/upload-artifact@v4
with:
name: aristo-badge
path: docs/badge.svg
";
#[aristo::intent(
"A second invocation never errors and never overwrites existing \
files. Each pre-existing artifact is noted; only missing ones get \
created.",
verify = "test",
id = "init_is_idempotent"
)]
pub(crate) fn run(_force: bool) -> CliResult<()> {
let cwd = std::env::current_dir()?;
let mut any_change = false;
create_or_note_file(
&cwd.join("aristo.toml"),
"aristo.toml",
serialize_default_config,
&mut any_change,
)?;
let aristo_dir = cwd.join(".aristo");
fs::create_dir_all(&aristo_dir)?;
create_or_note_file_custom_msg(
&aristo_dir.join("index.toml"),
"ok: created .aristo/index.toml (empty; 0 annotations)",
"note: .aristo/index.toml already exists — leaving as-is.",
serialize_initial_index,
&mut any_change,
)?;
create_or_note_dir(&aristo_dir.join("specs"), ".aristo/specs/", &mut any_change)?;
create_or_note_dir(&aristo_dir.join("doc"), ".aristo/doc/", &mut any_change)?;
let hook_dir = cwd.join(".git").join("hooks");
if hook_dir.is_dir() {
let hook_path = hook_dir.join("pre-commit");
if hook_path.exists() {
println!("note: pre-commit hook already installed.");
} else {
fs::write(&hook_path, PRE_COMMIT_HOOK)?;
make_executable(&hook_path)?;
println!("ok: installed pre-commit hook (.git/hooks/pre-commit)");
any_change = true;
}
}
let workflows_dir = cwd.join(".github").join("workflows");
let workflow_path = workflows_dir.join("aristo.yml");
if workflow_path.exists() {
println!("note: .github/workflows/aristo.yml already exists.");
} else {
fs::create_dir_all(&workflows_dir)?;
fs::write(&workflow_path, GH_WORKFLOW_STARTER)?;
println!("ok: wrote .github/workflows/aristo.yml (starter; edit freely)");
any_change = true;
}
if !any_change {
println!("ok: nothing to do.");
}
Ok(())
}
fn create_or_note_file<F>(
path: &Path,
label: &str,
content_fn: F,
any_change: &mut bool,
) -> CliResult<()>
where
F: FnOnce() -> CliResult<String>,
{
create_or_note_file_custom_msg(
path,
&format!("ok: created {label}"),
&format!("note: {label} already exists — leaving as-is."),
content_fn,
any_change,
)
}
fn create_or_note_file_custom_msg<F>(
path: &Path,
created_msg: &str,
exists_msg: &str,
content_fn: F,
any_change: &mut bool,
) -> CliResult<()>
where
F: FnOnce() -> CliResult<String>,
{
if path.exists() {
println!("{exists_msg}");
} else {
let content = content_fn()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, content)?;
println!("{created_msg}");
*any_change = true;
}
Ok(())
}
fn create_or_note_dir(path: &Path, label: &str, any_change: &mut bool) -> CliResult<()> {
if path.exists() {
println!("note: {label} already exists.");
} else {
fs::create_dir(path)?;
println!("ok: created {label}");
*any_change = true;
}
Ok(())
}
fn serialize_default_config() -> CliResult<String> {
let cfg = ConfigFile::default();
toml::to_string_pretty(&cfg).map_err(|e| {
CliError::Io(std::io::Error::other(format!(
"serializing aristo.toml: {e}"
)))
})
}
fn serialize_initial_index() -> CliResult<String> {
let index = IndexFile {
meta: Meta {
schema_version: 1,
generated_by: Some(format!("aristo init {}", env!("CARGO_PKG_VERSION"))),
generated_at: Some(now_rfc3339()),
source_root: None,
},
entries: BTreeMap::new(),
};
toml::to_string_pretty(&index).map_err(|e| {
CliError::Io(std::io::Error::other(format!(
"serializing .aristo/index.toml: {e}"
)))
})
}
fn now_rfc3339() -> String {
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;
OffsetDateTime::now_utc()
.format(&Rfc3339)
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string())
}
#[cfg(unix)]
fn make_executable(path: &Path) -> CliResult<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(path, perms)?;
Ok(())
}
#[cfg(not(unix))]
fn make_executable(_path: &Path) -> CliResult<()> {
Ok(())
}