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 lite PR gate (starter; edit freely). Generated by `aristo init --ci`.
#
# Runs the cheap, local annotation checks — stamp/lint/doc --check — via the
# shared aristo-action. No token needed; runs entirely on the runner.
#
# Heavy canon verification (`aristo verify`) is a separate workflow: run
# `aristo init --ci-verify` to also generate aristo-verify.yml.
name: aristo
on: [push, pull_request]
jobs:
aristo:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aretta-ai/aristo-action@v1
with:
checks: stamp, lint, doc
";
const GH_VERIFY_WORKFLOW: &str = "\
# Aristo heavy verification (starter; edit freely).
# Generated by `aristo init --ci-verify`.
#
# Runs `aristo verify` (server-side canon conformance) for the bound
# annotations in .aristo/index.toml. Manual + nightly.
#
# Setup: add a repository secret ARETTA_TOKEN (your arta_* token; paid tier).
# NOT on pull_request — verify needs the checked-out commit pushed to origin.
name: aristo verify
on:
workflow_dispatch:
schedule:
- cron: '0 7 * * *' # nightly at 07:00 UTC
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aretta-ai/aristo-action@v1
with:
checks: verify
aretta-token: ${{ secrets.ARETTA_TOKEN }}
";
#[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, ci: bool, ci_verify: 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");
if ci || ci_verify {
create_or_note_file_custom_msg(
&workflows_dir.join("aristo.yml"),
"ok: wrote .github/workflows/aristo.yml (lite PR gate; edit freely)",
"note: .github/workflows/aristo.yml already exists.",
|| Ok(GH_WORKFLOW_STARTER.to_string()),
&mut any_change,
)?;
}
if ci_verify {
create_or_note_file_custom_msg(
&workflows_dir.join("aristo-verify.yml"),
"ok: wrote .github/workflows/aristo-verify.yml (heavy verify; needs ARETTA_TOKEN secret; edit freely)",
"note: .github/workflows/aristo-verify.yml already exists.",
|| Ok(GH_VERIFY_WORKFLOW.to_string()),
&mut any_change,
)?;
print_verify_token_help(&cwd);
}
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 print_verify_token_help(cwd: &Path) {
println!();
println!("Next — the verify workflow needs an ARETTA_TOKEN secret on this repo:");
match aristo_core::auth::derive_repo_full_name(cwd) {
Ok(repo) => println!(
" 1. Add the secret: https://github.com/{repo}/settings/secrets/actions/new (name: ARETTA_TOKEN)"
),
Err(_) => println!(
" 1. Add the secret under Settings -> Secrets and variables -> Actions -> New repository secret (name: ARETTA_TOKEN)"
),
}
println!(
" 2. Token value: `aristo auth token` prints yours — pipe to your clipboard, e.g. `aristo auth token | pbcopy`"
);
println!(" ...or `aristo auth login` to mint a new one.");
}
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(())
}