pray-cli 1.12.0

Package manager for the language placed before inference — Prayfile CLI (binary: pray)
use crate::auth_client::{login_with_passkey, login_with_ssh_agent};
use crate::materialize::{materialize_package_directory, write_package_archive};
use crate::project_paths::{
    lockfile_path, manifest_path, resolve_project, resolve_project_with_options, workspace_root,
};
use pray_core::hashing::normalize_line_endings;
use pray_core::lockfile::read_lockfile;
use pray_core::manifest::{parse_manifest, read_manifest_text};
use pray_core::resolve_context::ResolveOptions;
use pray_core::{PrayError, PrayResult};
use std::fs;
use std::path::{Path, PathBuf};

pub(crate) fn format_command() -> PrayResult<()> {
    let path = manifest_path();
    let original = read_manifest_text(&path)?;
    let manifest = parse_manifest(&original)?;
    let project = resolve_project_with_options(
        &path,
        &ResolveOptions {
            offline: true,
            ..ResolveOptions::default()
        },
    )
    .or_else(|_| resolve_project(&path))?;
    let hints = pray_core::format_manifest::classify_format_hints(&project);
    let formatted = pray_core::format_manifest::format_recommended(&manifest, &hints)?;
    if formatted != original {
        fs::write(&path, formatted)?;
    }

    if let Ok(lockfile) = read_lockfile(&lockfile_path()) {
        for target in &lockfile.target {
            for output in &target.outputs {
                let output_path = Path::new(output);
                if !output_path.exists() {
                    continue;
                }
                let original_output = fs::read_to_string(output_path)?;
                let formatted_output =
                    format_marker_comments(&normalize_line_endings(&original_output));
                if formatted_output != original_output {
                    fs::write(output_path, formatted_output)?;
                }
            }
        }
    }
    Ok(())
}

pub(crate) fn package_command() -> PrayResult<()> {
    let project = resolve_project(&manifest_path())?;
    for package in &project.packages {
        let output_path = package_archive_path(&package.declaration.name, &package.spec.version);
        write_package_archive(package, &output_path)?;
    }
    Ok(())
}

pub(crate) fn login_command(
    servers: Vec<String>,
    email: String,
    credential_id: Option<String>,
    passkey_key: Option<PathBuf>,
    public_key: Option<PathBuf>,
    ssh_agent: bool,
) -> PrayResult<()> {
    let session_root = workspace_root();
    for server in servers {
        let session = if let Some(passkey_key) = &passkey_key {
            let credential_id = credential_id.as_ref().ok_or_else(|| {
                PrayError::Unsupported("passkey login requires --credential-id".to_string())
            })?;
            login_with_passkey(&server, credential_id, passkey_key, &session_root)?
        } else if ssh_agent {
            let public_key = public_key.as_ref().ok_or_else(|| {
                PrayError::Unsupported("ssh-agent login requires --public-key".to_string())
            })?;
            login_with_ssh_agent(&server, public_key, &session_root)?
        } else {
            return Err(PrayError::Unsupported(
                "login requires an authentication mode".to_string(),
            ));
        };
        if session.email != email {
            return Err(PrayError::Resolution(format!(
                "login completed for {} but {} was requested",
                session.email, email
            )));
        }
        println!(
            "logged in as {} via {} on {}",
            session.email, session.kind, server
        );
    }
    Ok(())
}

pub(crate) fn vendor_command() -> PrayResult<()> {
    let project = resolve_project(&manifest_path())?;
    for package in &project.packages {
        let output_directory =
            vendor_package_path(&package.declaration.name, &package.spec.version);
        materialize_package_directory(package, &output_directory)?;
    }
    Ok(())
}

fn package_archive_path(name: &str, version: &str) -> PathBuf {
    PathBuf::from(format!("{}-{}.praypkg", name.replace('/', "-"), version))
}

fn vendor_package_path(name: &str, version: &str) -> PathBuf {
    PathBuf::from(".pray/vendor")
        .join(name.replace('/', "-"))
        .join(version)
}

fn format_marker_comments(text: &str) -> String {
    let lines: Vec<String> = text
        .split('\n')
        .map(|line| match canonical_marker_line(line) {
            Some(marker) => marker,
            None => line.to_string(),
        })
        .collect();
    let mut output = lines.join("\n");
    if !output.ends_with('\n') {
        output.push('\n');
    }
    output
}

fn canonical_marker_line(line: &str) -> Option<String> {
    let trimmed = line.trim();
    let remainder = trimmed.strip_prefix("<!--")?.trim_start();
    let remainder = remainder.strip_prefix("pray:")?;
    let content = remainder.strip_suffix("-->")?.trim();
    if content == "0 ignore-comments" {
        return Some("<!-- pray:0 ignore-comments -->".to_string());
    }
    if content
        .chars()
        .all(|character| character.is_ascii_lowercase() || character.is_ascii_digit())
    {
        return Some(format!("<!-- pray:{content} -->"));
    }
    None
}