use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use regex::Regex;
use serde::Serialize;
pub const TYPESCRIPT_SDK: &str = "@usearete/sdk";
pub const TYPESCRIPT_REACT: &str = "@usearete/react";
pub const TYPESCRIPT_ADAPTER_WEB3JS: &str = "@usearete/adapter-web3js";
pub const TYPESCRIPT_ADAPTER_KIT: &str = "@usearete/adapter-kit";
pub const TYPESCRIPT_LOCKSTEP: [&str; 4] = [
TYPESCRIPT_SDK,
TYPESCRIPT_REACT,
TYPESCRIPT_ADAPTER_WEB3JS,
TYPESCRIPT_ADAPTER_KIT,
];
pub const ZOD: &str = "zod";
pub const ZOD_RANGE: &str = "^3.24.1";
pub const RUST_SDK_CRATE: &str = "arete-a4-sdk";
pub const PYTHON_SDK_DISTRIBUTION: &str = "arete-sdk";
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RuntimePackage {
pub package: String,
pub version: String,
}
pub fn lockstep_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub fn lockstep_extension_api() -> u32 {
arete_sdk::EXTENSION_API_VERSION
}
pub fn nearest_package_json(start: &Path) -> Option<PathBuf> {
start
.ancestors()
.map(|ancestor| ancestor.join("package.json"))
.find(|candidate| candidate.is_file())
}
pub fn declared_dependencies(package_json: &Path) -> BTreeSet<String> {
let Some(value) = fs::read(package_json)
.ok()
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(&bytes).ok())
else {
return BTreeSet::new();
};
[
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
]
.iter()
.filter_map(|section| value.get(section).and_then(serde_json::Value::as_object))
.flat_map(|section| section.keys().cloned())
.collect()
}
pub fn typescript_runtime_set(declared: &BTreeSet<String>) -> Vec<RuntimePackage> {
typescript_runtime_set_at(declared, lockstep_version())
}
pub fn typescript_runtime_set_at(
declared: &BTreeSet<String>,
version: &str,
) -> Vec<RuntimePackage> {
let lockstep = |package: &str| RuntimePackage {
package: package.to_string(),
version: version.to_string(),
};
let mut set = vec![lockstep(TYPESCRIPT_SDK)];
if declared.contains("react") {
set.push(lockstep(TYPESCRIPT_REACT));
}
if declared.iter().any(|package| {
package == "@solana/web3.js" || package.starts_with("@solana/wallet-adapter")
}) {
set.push(lockstep(TYPESCRIPT_ADAPTER_WEB3JS));
}
if declared.contains("@solana/kit") {
set.push(lockstep(TYPESCRIPT_ADAPTER_KIT));
}
set.push(RuntimePackage {
package: ZOD.to_string(),
version: ZOD_RANGE.to_string(),
});
set
}
pub fn typescript_runtime_for_outputs<'a>(
outputs: impl IntoIterator<Item = &'a Path>,
) -> Vec<RuntimePackage> {
let mut any = false;
let mut declared = BTreeSet::new();
for output in outputs {
any = true;
declared.extend(app_dependencies(output));
}
if any {
typescript_runtime_set(&declared)
} else {
Vec::new()
}
}
pub fn app_dependencies(output: &Path) -> BTreeSet<String> {
nearest_package_json(output)
.map(|package_json| declared_dependencies(&package_json))
.unwrap_or_default()
}
pub fn npm_install_command(packages: &[RuntimePackage]) -> String {
let mut command = "npm install".to_string();
for package in packages {
let spec = format!("{}@{}", package.package, package.version);
let plain = spec
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '@' | '/' | '.' | '-' | '_'));
command.push(' ');
if plain {
command.push_str(&spec);
} else {
command.push('"');
command.push_str(&spec);
command.push('"');
}
}
command
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledRuntime {
pub version: String,
pub extension_api: Option<u32>,
pub location: PathBuf,
}
pub fn read_installed_npm_package(package_dir: &Path) -> Option<InstalledRuntime> {
let manifest = package_dir.join("package.json");
let value: serde_json::Value = serde_json::from_slice(&fs::read(&manifest).ok()?).ok()?;
Some(InstalledRuntime {
version: value.get("version")?.as_str()?.to_string(),
extension_api: value
.pointer("/arete/extensionApi")
.and_then(serde_json::Value::as_u64)
.and_then(|value| u32::try_from(value).ok()),
location: manifest,
})
}
pub fn resolve_npm_package(start: &Path, package: &str) -> Option<PathBuf> {
start
.ancestors()
.map(|ancestor| ancestor.join("node_modules").join(package))
.find(|candidate| candidate.join("package.json").is_file())
}
pub fn installed_typescript_sdk(start: &Path) -> Option<InstalledRuntime> {
read_installed_npm_package(&resolve_npm_package(start, TYPESCRIPT_SDK)?)
}
pub fn installed_rust_sdk(start: &Path) -> Option<InstalledRuntime> {
for ancestor in start.ancestors() {
let Some(manifest) = read_toml(&ancestor.join("Cargo.toml")) else {
continue;
};
if let Some(path) = rust_sdk_path(&manifest) {
return read_rust_sdk_manifest(&ancestor.join(path).join("Cargo.toml"));
}
}
let lock = start
.ancestors()
.map(|ancestor| ancestor.join("Cargo.lock"))
.find(|candidate| candidate.is_file())?;
let version = locked_registry_version(&read_toml(&lock)?)?;
let cargo_home = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|home| home.join(".cargo")))?;
let sources = fs::read_dir(cargo_home.join("registry").join("src")).ok()?;
sources
.filter_map(|entry| entry.ok())
.map(|entry| {
entry
.path()
.join(format!("{RUST_SDK_CRATE}-{version}"))
.join("Cargo.toml")
})
.find(|candidate| candidate.is_file())
.and_then(|manifest| read_rust_sdk_manifest(&manifest))
}
fn read_toml(path: &Path) -> Option<toml::Table> {
fs::read_to_string(path).ok()?.parse().ok()
}
fn rust_sdk_path(manifest: &toml::Table) -> Option<String> {
let mut tables: Vec<&toml::Table> = Vec::new();
for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
tables.extend(manifest.get(section).and_then(toml::Value::as_table));
}
if let Some(targets) = manifest.get("target").and_then(toml::Value::as_table) {
for target in targets.values().filter_map(toml::Value::as_table) {
for section in ["dependencies", "dev-dependencies", "build-dependencies"] {
tables.extend(target.get(section).and_then(toml::Value::as_table));
}
}
}
if let Some(workspace) = manifest.get("workspace").and_then(toml::Value::as_table) {
tables.extend(
workspace
.get("dependencies")
.and_then(toml::Value::as_table),
);
}
if let Some(patches) = manifest.get("patch").and_then(toml::Value::as_table) {
tables.extend(patches.values().filter_map(toml::Value::as_table));
}
tables
.iter()
.flat_map(|table| table.iter())
.find_map(|(key, value)| {
let dependency = value.as_table()?;
let package = dependency
.get("package")
.and_then(toml::Value::as_str)
.unwrap_or(key);
(package == RUST_SDK_CRATE)
.then(|| dependency.get("path").and_then(toml::Value::as_str))
.flatten()
.map(str::to_string)
})
}
fn locked_registry_version(lock: &toml::Table) -> Option<String> {
let mut versions = lock
.get("package")?
.as_array()?
.iter()
.filter_map(toml::Value::as_table)
.filter(|package| {
package.get("name").and_then(toml::Value::as_str) == Some(RUST_SDK_CRATE)
&& package
.get("source")
.and_then(toml::Value::as_str)
.is_some_and(|source| source.starts_with("registry+"))
})
.filter_map(|package| package.get("version").and_then(toml::Value::as_str));
let version = versions.next()?.to_string();
versions.next().is_none().then_some(version)
}
fn read_rust_sdk_manifest(path: &Path) -> Option<InstalledRuntime> {
let manifest = read_toml(path)?;
let package = manifest.get("package")?.as_table()?;
if package.get("name").and_then(toml::Value::as_str) != Some(RUST_SDK_CRATE) {
return None;
}
Some(InstalledRuntime {
version: package.get("version")?.as_str()?.to_string(),
extension_api: package
.get("metadata")
.and_then(|metadata| metadata.get("arete"))
.and_then(|arete| arete.get("extension-api"))
.and_then(toml::Value::as_integer)
.and_then(|value| u32::try_from(value).ok()),
location: path.to_path_buf(),
})
}
pub fn installed_python_sdk(start: &Path) -> Option<InstalledRuntime> {
let mut environments = Vec::new();
environments.extend(std::env::var_os("VIRTUAL_ENV").map(PathBuf::from));
environments.extend(project_virtualenvs(start));
environments
.iter()
.find_map(|environment| python_sdk_in_environment(environment))
}
fn project_virtualenvs(start: &Path) -> Vec<PathBuf> {
start
.ancestors()
.flat_map(|ancestor| [ancestor.join(".venv"), ancestor.join("venv")])
.filter(|candidate| candidate.join("pyvenv.cfg").is_file())
.collect()
}
fn python_sdk_in_environment(environment: &Path) -> Option<InstalledRuntime> {
let mut site_packages = vec![environment.join("Lib").join("site-packages")];
if let Ok(entries) = fs::read_dir(environment.join("lib")) {
let mut versions = entries
.filter_map(|entry| entry.ok())
.map(|entry| entry.path().join("site-packages"))
.collect::<Vec<_>>();
versions.sort();
site_packages.extend(versions);
}
site_packages
.iter()
.find_map(|site| python_sdk_in_site_packages(site))
}
fn python_sdk_in_site_packages(site: &Path) -> Option<InstalledRuntime> {
let prefix = format!("{}-", PYTHON_SDK_DISTRIBUTION.replace('-', "_"));
let version = fs::read_dir(site)
.ok()?
.filter_map(|entry| entry.ok())
.filter_map(|entry| entry.file_name().into_string().ok())
.find_map(|name| {
name.strip_suffix(".dist-info")?
.strip_prefix(&prefix)
.map(str::to_string)
})?;
let module = site.join("arete").join("extensions.py");
let source = fs::read_to_string(&module).ok()?;
let declared = Regex::new(r"(?m)^EXTENSION_API_VERSION\s*(?::[^=]+)?=\s*(\d+)")
.expect("extension API regex should compile");
Some(InstalledRuntime {
version,
extension_api: declared
.captures(&source)
.and_then(|captures| captures[1].parse().ok()),
location: module,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn write(path: &Path, contents: &str) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, contents).unwrap();
}
#[test]
fn zod_range_matches_the_typescript_sdk() {
let manifest =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../typescript/core/package.json");
let Ok(bytes) = fs::read(&manifest) else {
return; };
let value: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(value["dependencies"]["zod"], ZOD_RANGE);
assert_eq!(value["version"], lockstep_version());
assert_eq!(
value["arete"]["extensionApi"].as_u64(),
Some(u64::from(lockstep_extension_api()))
);
}
#[test]
fn runtime_set_follows_the_app_dependencies() {
let version = lockstep_version();
let names = |declared: &[&str]| {
typescript_runtime_set(&declared.iter().map(|name| name.to_string()).collect())
.into_iter()
.map(|package| format!("{}@{}", package.package, package.version))
.collect::<Vec<_>>()
};
assert_eq!(
names(&[]),
vec![
format!("@usearete/sdk@{version}"),
"zod@^3.24.1".to_string()
]
);
assert_eq!(
names(&["react", "@solana/wallet-adapter-react"]),
vec![
format!("@usearete/sdk@{version}"),
format!("@usearete/react@{version}"),
format!("@usearete/adapter-web3js@{version}"),
"zod@^3.24.1".to_string(),
]
);
assert_eq!(
names(&["@solana/kit"]),
vec![
format!("@usearete/sdk@{version}"),
format!("@usearete/adapter-kit@{version}"),
"zod@^3.24.1".to_string(),
]
);
}
#[test]
fn runtime_for_outputs_reads_the_nearest_package_json() {
let temp = tempfile::tempdir().unwrap();
write(
&temp.path().join("app/package.json"),
r#"{"dependencies":{"react":"^19"},"devDependencies":{"@solana/web3.js":"^1.95"}}"#,
);
let output = temp.path().join("app/src/arete/ore");
let set = typescript_runtime_for_outputs([output.as_path()]);
let packages = set.iter().map(|p| p.package.as_str()).collect::<Vec<_>>();
assert_eq!(
packages,
vec![
"@usearete/sdk",
"@usearete/react",
"@usearete/adapter-web3js",
"zod"
]
);
assert!(typescript_runtime_for_outputs(std::iter::empty()).is_empty());
}
#[test]
fn npm_install_command_quotes_ranges_only() {
let command = npm_install_command(&[
RuntimePackage {
package: "@usearete/sdk".into(),
version: "0.23.0".into(),
},
RuntimePackage {
package: "zod".into(),
version: "^3.24.1".into(),
},
]);
assert_eq!(command, r#"npm install @usearete/sdk@0.23.0 "zod@^3.24.1""#);
}
#[test]
fn installed_typescript_sdk_reads_the_resolved_copy() {
let temp = tempfile::tempdir().unwrap();
write(
&temp.path().join("node_modules/@usearete/sdk/package.json"),
r#"{"name":"@usearete/sdk","version":"0.23.0","arete":{"extensionApi":1}}"#,
);
let installed = installed_typescript_sdk(&temp.path().join("src/generated")).unwrap();
assert_eq!(installed.version, "0.23.0");
assert_eq!(installed.extension_api, Some(1));
write(
&temp
.path()
.join("old/node_modules/@usearete/sdk/package.json"),
r#"{"name":"@usearete/sdk","version":"0.20.3"}"#,
);
let older = installed_typescript_sdk(&temp.path().join("old/src")).unwrap();
assert_eq!(older.version, "0.20.3");
assert_eq!(older.extension_api, None);
}
#[test]
fn installed_rust_sdk_follows_path_dependencies_and_patches() {
let temp = tempfile::tempdir().unwrap();
write(
&temp.path().join("sdk/Cargo.toml"),
"[package]\nname = \"arete-a4-sdk\"\nversion = \"0.23.0\"\n\n[package.metadata.arete]\nextension-api = 1\n",
);
write(
&temp.path().join("app/Cargo.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n\n[dependencies]\narete-sdk = { package = \"arete-a4-sdk\", version = \"0\" }\n\n[patch.crates-io]\narete-a4-sdk = { path = \"../sdk\" }\n",
);
let installed = installed_rust_sdk(&temp.path().join("app/src/generated")).unwrap();
assert_eq!(installed.version, "0.23.0");
assert_eq!(installed.extension_api, Some(1));
}
#[test]
fn locked_registry_version_requires_one_registry_release() {
let lock = |body: &str| body.parse::<toml::Table>().unwrap();
assert_eq!(
locked_registry_version(&lock(
"[[package]]\nname = \"arete-a4-sdk\"\nversion = \"0.22.1\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n"
)),
Some("0.22.1".to_string())
);
assert_eq!(
locked_registry_version(&lock(
"[[package]]\nname = \"arete-a4-sdk\"\nversion = \"0.22.1\"\n"
)),
None
);
}
#[test]
fn installed_python_sdk_reads_a_project_virtualenv() {
let temp = tempfile::tempdir().unwrap();
let site = temp.path().join(".venv/lib/python3.12/site-packages");
write(&temp.path().join(".venv/pyvenv.cfg"), "home = /usr/bin\n");
write(&site.join("arete_sdk-0.23.0.dist-info/METADATA"), "");
write(
&site.join("arete/extensions.py"),
"\"\"\"Extensions.\"\"\"\n\nEXTENSION_API_VERSION = 1\n",
);
let environments = project_virtualenvs(&temp.path().join("app"));
let installed = python_sdk_in_environment(&environments[0]).unwrap();
assert_eq!(installed.version, "0.23.0");
assert_eq!(installed.extension_api, Some(1));
}
}