Codex Handbook
tui/src/npm_registry.rs 130 lines
use serde::Deserialize;use std::collections::HashMap;#[cfg(not(debug_assertions))]pub(crate) const PACKAGE_URL: &str = "https://registry.npmjs.org/@openai%2fcodex";#[derive(Deserialize, Debug, Clone)]pub(crate) struct NpmPackageInfo {    #[serde(rename = "dist-tags")]    dist_tags: HashMap<String, String>,    versions: HashMap<String, NpmPackageVersionInfo>,}#[derive(Deserialize, Debug, Clone)]struct NpmPackageVersionInfo {    dist: Option<NpmPackageDist>,}#[derive(Deserialize, Debug, Clone)]struct NpmPackageDist {    tarball: Option<String>,    integrity: Option<String>,}pub(crate) fn ensure_version_ready(    package_info: &NpmPackageInfo,    version: &str,) -> anyhow::Result<()> {    let version = version.trim();    match package_info.dist_tags.get("latest").map(String::as_str) {        Some(latest) if latest == version => {}        Some(latest) => anyhow::bail!(            "npm latest dist-tag points to {latest}, expected GitHub release {version}"        ),        None => anyhow::bail!("npm package is missing latest dist-tag"),    }    version_info_with_dist(package_info, version)?;    Ok(())}fn version_info_with_dist<'a>(    package_info: &'a NpmPackageInfo,    version: &str,) -> anyhow::Result<&'a NpmPackageVersionInfo> {    let info = package_info        .versions        .get(version)        .ok_or_else(|| anyhow::anyhow!("npm package version {version} is missing"))?;    let Some(dist) = info.dist.as_ref() else {        anyhow::bail!("npm package version {version} is missing dist metadata");    };    let has_tarball = dist        .tarball        .as_deref()        .is_some_and(|tarball| !tarball.is_empty());    if !has_tarball {        anyhow::bail!("npm package version {version} is missing dist.tarball");    }    let has_integrity = dist        .integrity        .as_ref()        .is_some_and(|integrity| !integrity.is_empty());    if !has_integrity {        anyhow::bail!("npm package version {version} is missing dist.integrity");    }    Ok(info)}#[cfg(test)]mod tests {    use super::*;    fn version_json(version: &str) -> serde_json::Value {        serde_json::json!({            "dist": {                "integrity": format!("sha512-{version}"),                "tarball": format!("https://registry.npmjs.org/@openai/codex/-/codex-{version}.tgz"),            }        })    }    fn package_info(github_latest: &str, npm_latest: &str) -> NpmPackageInfo {        let mut versions = serde_json::Map::new();        versions.insert(github_latest.to_string(), version_json(github_latest));        serde_json::from_value(serde_json::json!({            "dist-tags": { "latest": npm_latest },            "versions": serde_json::Value::Object(versions),        }))        .expect("valid npm package metadata")    }    #[test]    fn ready_version_requires_latest_dist_tag_and_root_dist() {        let latest = "1.2.3";        let package_info = package_info(latest, latest);        ensure_version_ready(&package_info, latest).expect("npm package is ready");    }    #[test]    fn ready_version_rejects_stale_latest_dist_tag() {        let package_info = package_info("1.2.3", "1.2.2");        let err = ensure_version_ready(&package_info, "1.2.3")            .expect_err("npm latest dist-tag must match GitHub latest");        assert!(            err.to_string().contains("latest dist-tag"),            "error should name stale latest dist-tag: {err}"        );    }    #[test]    fn ready_version_rejects_missing_root_dist() {        let package_info: NpmPackageInfo = serde_json::from_value(serde_json::json!({            "dist-tags": { "latest": "1.2.3" },            "versions": { "1.2.3": {} },        }))        .expect("valid npm package metadata");        let err = ensure_version_ready(&package_info, "1.2.3")            .expect_err("root package must have dist metadata");        assert!(            err.to_string().contains("missing dist metadata"),            "error should name missing dist metadata: {err}"        );    }}