pray-cli 1.22.0

Package manager for the language placed before inference — Prayfile CLI (binary: pray)
#[path = "install_network_support.rs"]
mod support;

use serde_json::Value;
use std::fs;
use std::path::PathBuf;
use std::process::Command;

use support::temporary_directory;

fn run_pray(directory: &std::path::Path, arguments: &[&str]) -> std::process::Output {
    let binary = PathBuf::from(env!("CARGO_BIN_EXE_pray"));
    let binary = binary.canonicalize().unwrap_or(binary);
    Command::new(binary)
        .args(arguments)
        .current_dir(directory)
        .output()
        .expect("run pray")
}

#[test]
fn prayer_init_scaffolds_a_package_repository_layout() {
    let repository = temporary_directory("pray-prayer-init");

    let output = run_pray(&repository, &["prayer", "init"]);
    assert!(
        output.status.success(),
        "prayer init failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let prayspec_files: Vec<_> = fs::read_dir(&repository)
        .expect("read repository")
        .filter_map(|entry| entry.ok())
        .map(|entry| entry.path())
        .filter(|path| path.extension().and_then(|value| value.to_str()) == Some("prayspec"))
        .collect();
    assert_eq!(prayspec_files.len(), 1);
    assert!(repository.join("README.md").is_file());
    assert!(repository.join("exports").is_dir());
}

#[test]
fn prayer_init_in_a_project_writes_an_unversioned_path_package() {
    let repository = temporary_directory("pray-prayer-init-local");
    fs::write(repository.join("Prayfile"), "prayfile \"1\"\n").expect("Prayfile");

    let output = run_pray(&repository, &["prayer", "init"]);
    assert!(
        output.status.success(),
        "prayer init failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let prayspec = fs::read_to_string(repository.join("prayers/project/project.prayspec"))
        .expect("local prayspec");
    assert!(prayspec.contains("spec.name = \"local/project\""));
    assert!(!prayspec.contains("spec.version"));
    assert!(repository
        .join("prayers/project/exports/project.md")
        .is_file());

    let manifest = fs::read_to_string(repository.join("Prayfile")).expect("Prayfile");
    assert!(manifest.contains("source \"local\", path: \"prayers\""));
    assert!(manifest.contains("pray \"local/project\""));
    assert!(!manifest.contains("path: \"prayers/project\""));
}

#[test]
fn prayer_init_uses_an_author_chosen_path_source_directory() {
    let repository = temporary_directory("pray-prayer-init-path");
    fs::write(repository.join("Prayfile"), "prayfile \"1\"\n").expect("Prayfile");

    let output = run_pray(
        &repository,
        &["prayer", "init", "notes", "--path", "guidance"],
    );
    assert!(
        output.status.success(),
        "prayer init failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    assert!(repository.join("guidance/notes/notes.prayspec").is_file());
    let manifest = fs::read_to_string(repository.join("Prayfile")).expect("Prayfile");
    assert!(manifest.contains("source \"local\", path: \"guidance\""));
    assert!(manifest.contains("pray \"local/notes\""));
    assert!(!manifest.contains("path: \"guidance/notes\""));
}

#[test]
fn prayer_init_declares_the_prayer_once_inside_compose() {
    let repository = temporary_directory("pray-prayer-init-compose");
    fs::write(
        repository.join("Prayfile"),
        "prayfile \"1\"\ncompose \"AGENTS.md\" do\n  pray \".agents/project.md\"\nend\n",
    )
    .expect("Prayfile");

    let output = run_pray(&repository, &["prayer", "init"]);
    assert!(
        output.status.success(),
        "prayer init failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let manifest = fs::read_to_string(repository.join("Prayfile")).expect("Prayfile");
    let declarations = manifest.matches("pray \"local/project\"").count();
    assert_eq!(
        declarations, 1,
        "expected one pray \"local/project\", got:\n{manifest}"
    );
    assert!(manifest.contains("compose \"AGENTS.md\" do\n  pray \"local/project\""));
    assert!(!manifest.contains("path: \"prayers/project\""));
}

#[test]
fn prayer_init_reuses_an_existing_path_source() {
    let repository = temporary_directory("pray-prayer-init-existing-source");
    fs::write(
        repository.join("Prayfile"),
        "prayfile \"1\"\nsource \"local\", path: \"guidance\"\n",
    )
    .expect("Prayfile");

    let output = run_pray(&repository, &["prayer", "init"]);
    assert!(
        output.status.success(),
        "prayer init failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    assert!(repository
        .join("guidance/project/project.prayspec")
        .is_file());
    let manifest = fs::read_to_string(repository.join("Prayfile")).expect("Prayfile");
    assert_eq!(
        manifest.matches("source \"local\"").count(),
        1,
        "expected one path source, got:\n{manifest}"
    );
    assert!(!repository.join("prayers/project").exists());
}

#[test]
fn prayer_init_install_locks_local_and_package_refuses() {
    let repository = temporary_directory("pray-prayer-init-install");
    fs::write(repository.join("Prayfile"), "prayfile \"1\"\n").expect("Prayfile");

    let init = run_pray(&repository, &["prayer", "init", "notes"]);
    assert!(
        init.status.success(),
        "prayer init failed: {}",
        String::from_utf8_lossy(&init.stderr)
    );

    let install = run_pray(&repository, &["install"]);
    assert!(
        install.status.success(),
        "install failed: {}",
        String::from_utf8_lossy(&install.stderr)
    );
    let lock = fs::read_to_string(repository.join("Prayfile.lock")).expect("lock");
    assert!(
        lock.contains("name = \"local/notes\"") && lock.contains("version = \"local\""),
        "expected local lock version, got:\n{lock}"
    );

    let package = run_pray(&repository, &["package"]);
    assert!(!package.status.success());
    let stderr = String::from_utf8_lossy(&package.stderr);
    assert!(
        stderr.contains("needs a version"),
        "expected pack refusal, got: {stderr}"
    );
}

#[test]
fn prayer_init_refuses_distribution_layout_name() {
    let repository = temporary_directory("pray-prayer-init-v1");
    fs::write(repository.join("Prayfile"), "prayfile \"1\"\n").expect("Prayfile");

    let output = run_pray(&repository, &["prayer", "init", "v1"]);
    assert!(!output.status.success());
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(
        stderr.contains("reserved"),
        "expected reserved-name error, got: {stderr}"
    );
}

#[test]
fn repo_init_scaffolds_a_distribution_repository_layout() {
    let repository = temporary_directory("pray-repo-init");

    let output = run_pray(&repository, &["repo", "init"]);
    assert!(
        output.status.success(),
        "repo init failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    assert!(repository.join("prayers").is_dir());
    assert!(repository.join("prayers/v1/packages").is_dir());
    assert!(repository.join("prayers/v1/artifacts").is_dir());

    let index_text = fs::read_to_string(repository.join("prayers/v1/index.json")).expect("index");
    let index: Value = serde_json::from_str(&index_text).expect("index json");
    assert_eq!(index["spec"], "prayfile-distribution-1");
    assert!(index["packages"].as_array().expect("packages").is_empty());

    let trust_text = fs::read_to_string(repository.join("prayers/v1/trust.json")).expect("trust");
    let trust: Value = serde_json::from_str(&trust_text).expect("trust json");
    assert_eq!(trust["email_confirmation"], "required");
    assert!(!trust["passkeys_enabled"].as_bool().expect("passkeys"));
    assert!(!trust["ssh_keys_enabled"].as_bool().expect("ssh keys"));
    assert!(!trust["ssh_agent_signing_enabled"]
        .as_bool()
        .expect("ssh agent"));

    let distribution_text =
        fs::read_to_string(repository.join("prayers/v1/distribution.json")).expect("distribution");
    let distribution: Value = serde_json::from_str(&distribution_text).expect("distribution json");
    assert_eq!(distribution["spec"], "pray-distribution-config-1");
    assert!(distribution["protocols"]
        .as_array()
        .expect("protocols")
        .is_empty());
}