pray-cli 1.10.0

Package manager for the language placed before inference — Prayfile CLI (binary: pray)
use pray_core::project_context::{ProjectInvocationContext, ProjectInvocationOptions};
use pray_core::resolve::{resolve_project_in_context, ResolvedProject};
use pray_core::resolve_context::ResolveOptions;
use pray_core::{PrayError, PrayResult};
use std::cell::RefCell;
use std::path::PathBuf;

thread_local! {
    static INVOCATION: RefCell<Option<ProjectInvocationContext>> = const { RefCell::new(None) };
}

pub fn initialize(arguments: Vec<String>) -> PrayResult<Vec<String>> {
    let (options, remaining) = parse_global_options(arguments)?;
    let context = ProjectInvocationContext::from_options(options)?;
    INVOCATION.with(|cell| {
        *cell.borrow_mut() = Some(context);
    });
    Ok(remaining)
}

pub fn manifest_path() -> PathBuf {
    invocation_context().manifest_path
}

pub fn lockfile_path() -> PathBuf {
    invocation_context().lockfile_path()
}

pub fn project_root() -> PathBuf {
    invocation_context().project_root
}

pub fn resolve_current_project(options: &ResolveOptions) -> PrayResult<ResolvedProject> {
    let context = invocation_context();
    let mut options = options.clone();
    if options.environment.is_none() {
        options.environment = context.environment.clone();
    }
    let project =
        resolve_project_in_context(&context.manifest_path, &context.project_root, &options)?;
    emit_deprecation_warnings(&project);
    Ok(project)
}

fn emit_deprecation_warnings(project: &ResolvedProject) {
    for warning in project.manifest.deprecation_warnings() {
        eprintln!("{warning}");
    }
    let mut seen = std::collections::BTreeSet::new();
    for package in &project.packages {
        for warning in pray_core::deprecation::package_spec_deprecation_warnings(&package.spec) {
            if seen.insert(warning.clone()) {
                eprintln!("{warning}");
            }
        }
    }
}

fn invocation_context() -> ProjectInvocationContext {
    INVOCATION.with(|cell| {
        cell.borrow().clone().unwrap_or_else(|| {
            ProjectInvocationContext::from_current_directory().expect("project invocation context")
        })
    })
}

fn parse_global_options(
    arguments: Vec<String>,
) -> PrayResult<(ProjectInvocationOptions, Vec<String>)> {
    let mut options = ProjectInvocationOptions::default();
    let mut remaining = Vec::new();
    let mut iter = arguments.into_iter();
    while let Some(argument) = iter.next() {
        match argument.as_str() {
            "--path" => {
                options.project_root = Some(require_option_value("--path", iter.next())?);
            }
            "--file-path" => {
                options.manifest_path = Some(require_option_value("--file-path", iter.next())?);
            }
            "--env" | "--environment" => {
                options.environment = Some(require_environment_value(&argument, iter.next())?);
            }
            value if is_top_level_command(value) => {
                remaining.push(argument);
                remaining.extend(iter);
                break;
            }
            other if other.starts_with('-') => {
                remaining.push(argument);
                remaining.extend(iter);
                break;
            }
            _ => {
                remaining.push(argument);
                remaining.extend(iter);
                break;
            }
        }
    }
    Ok((options, remaining))
}

fn require_option_value(flag: &str, value: Option<String>) -> PrayResult<PathBuf> {
    value
        .map(PathBuf::from)
        .ok_or_else(|| PrayError::Usage(format!("{flag} requires a value")))
}

fn require_environment_value(flag: &str, value: Option<String>) -> PrayResult<String> {
    value.ok_or_else(|| PrayError::Usage(format!("{flag} requires a value")))
}

fn is_top_level_command(token: &str) -> bool {
    matches!(
        token,
        "manifest"
            | "init"
            | "prayer"
            | "repo"
            | "install"
            | "add"
            | "remove"
            | "update"
            | "unlock"
            | "render"
            | "plan"
            | "apply"
            | "verify"
            | "drift"
            | "format"
            | "package"
            | "publish"
            | "login"
            | "serve"
            | "confess"
            | "list"
            | "outdated"
            | "explain"
            | "vendor"
            | "clean"
            | "tree"
            | "sync"
            | "trust"
            | "version"
            | "-V"
            | "--version"
            | "-h"
            | "--help"
    )
}