refactor: rename aim to upm and extract appimage module

This commit is contained in:
stoorps 2026-03-21 22:39:11 +00:00
parent af13e98eb3
commit 863c57e473
Signed by: stoorps
SSH key fingerprint: SHA256:AZlPfu9hTu042EGtZElmDQoy+KvMOeShLDan/fYLoNI
117 changed files with 2622 additions and 887 deletions

View file

@ -1,66 +0,0 @@
use aim_cli::config::{CliConfig, ConfigError, SearchConfig, load_from_path};
use tempfile::tempdir;
#[test]
fn missing_config_file_returns_defaults() {
let dir = tempdir().unwrap();
let path = dir.path().join("config.toml");
let config = load_from_path(&path).unwrap();
assert_eq!(config, CliConfig::default());
assert_eq!(config.search, SearchConfig::default());
assert!(!config.allow_http);
assert!(config.search.bottom_to_top);
assert!(!config.search.skip_confirmation);
assert_eq!(config.theme.accent, "#b388ff");
assert_eq!(config.theme.accent_secondary, "#d5c2ff");
assert_eq!(config.theme.dim, "#7f7396");
}
#[test]
fn search_section_overrides_defaults() {
let dir = tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(
&path,
"allow_http = true\n\n[search]\nbottom_to_top = false\nskip_confirmation = true\n\n[theme]\naccent = \"#9f6bff\"\naccent_secondary = \"#efe7ff\"\ndim = \"#6b6480\"\n",
)
.unwrap();
let config = load_from_path(&path).unwrap();
assert_eq!(
config,
CliConfig {
allow_http: true,
search: SearchConfig {
bottom_to_top: false,
skip_confirmation: true,
},
theme: aim_cli::config::ThemeConfig {
accent: "#9f6bff".to_owned(),
accent_secondary: "#efe7ff".to_owned(),
dim: "#6b6480".to_owned(),
},
}
);
}
#[test]
fn malformed_toml_returns_path_aware_error() {
let dir = tempdir().unwrap();
let path = dir.path().join("config.toml");
std::fs::write(&path, "[search\nskip_confirmation = true\n").unwrap();
let error = load_from_path(&path).unwrap_err();
match error {
ConfigError::Parse {
path: error_path, ..
} => {
assert_eq!(error_path, path);
}
other => panic!("expected parse error, got {other:?}"),
}
}

View file

@ -1,21 +0,0 @@
use std::path::Path;
use aim_core::domain::app::InstallScope;
use aim_core::integration::paths::{desktop_entry_path, managed_appimage_path};
#[test]
fn user_scope_path_lands_under_home_managed_dir() {
let path = managed_appimage_path(Path::new("/home/test"), InstallScope::User, "bat");
assert_eq!(
path,
Path::new("/home/test/.local/lib/aim/appimages/bat.AppImage")
);
}
#[test]
fn system_scope_desktop_entry_uses_system_prefix() {
let path = desktop_entry_path(Path::new("/home/test"), InstallScope::System, "bat");
assert_eq!(path, Path::new("/usr/share/applications/aim-bat.desktop"));
}

View file

@ -0,0 +1,14 @@
[package]
name = "upm-appimage"
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
path = "src/lib.rs"
[dependencies]
quick-xml.workspace = true
reqwest.workspace = true
serde.workspace = true
upm-core = { path = "../upm-core" }

View file

@ -0,0 +1,163 @@
use crate::source::appimagehub::{
AppImageHubError, AppImageHubTransport, resolve_appimagehub_item, resolve_appimagehub_item_with,
};
use upm_core::adapters::traits::{
AdapterCapabilities, AdapterError, AdapterResolution, AdapterResolveOutcome, SourceAdapter,
};
use upm_core::app::providers::{ExternalAddProvider, ExternalAddResolution};
use upm_core::app::query::resolve_query;
use upm_core::domain::source::{ResolvedRelease, SourceKind, SourceRef};
use upm_core::domain::update::{
ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy,
};
pub struct AppImageHubAdapter;
impl AppImageHubAdapter {
pub fn resolve_source_with<T: AppImageHubTransport + ?Sized>(
&self,
source: &SourceRef,
transport: &T,
) -> Result<AdapterResolveOutcome, AdapterError> {
if source.kind != SourceKind::AppImageHub {
return Err(AdapterError::UnsupportedSource);
}
let resolved = resolve_appimagehub_item_with(source, transport)
.map_err(|error| AdapterError::ResolutionFailed(render_appimagehub_error(&error)))?;
match resolved {
Some(item) => Ok(AdapterResolveOutcome::Resolved(AdapterResolution {
source: item.source,
release: ResolvedRelease {
version: item.version,
prerelease: false,
},
})),
None => Ok(AdapterResolveOutcome::NoInstallableArtifact {
source: source.clone(),
}),
}
}
}
impl SourceAdapter for AppImageHubAdapter {
fn id(&self) -> &'static str {
"appimagehub"
}
fn capabilities(&self) -> AdapterCapabilities {
AdapterCapabilities {
supports_search: true,
supports_exact_resolution: true,
}
}
fn repository_source_kind(&self) -> Option<SourceKind> {
Some(SourceKind::AppImageHub)
}
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
if source.kind != SourceKind::AppImageHub {
return Err(AdapterError::UnsupportedQuery);
}
Ok(source)
}
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
match resolve_appimagehub_item(source)
.map_err(|error| AdapterError::ResolutionFailed(render_appimagehub_error(&error)))?
{
Some(item) => Ok(AdapterResolution {
source: item.source,
release: ResolvedRelease {
version: item.version,
prerelease: false,
},
}),
None => Err(AdapterError::ResolutionFailed(
"appimagehub item has no installable AppImage artifact".to_owned(),
)),
}
}
fn resolve_supported_source(
&self,
source: &SourceRef,
) -> Result<AdapterResolveOutcome, AdapterError> {
let transport = crate::source::appimagehub::default_transport();
self.resolve_source_with(source, transport.as_ref())
}
}
pub struct AppImageHubAddProvider<'a, T: AppImageHubTransport + ?Sized> {
transport: &'a T,
}
impl<'a, T: AppImageHubTransport + ?Sized> AppImageHubAddProvider<'a, T> {
pub fn new(transport: &'a T) -> Self {
Self { transport }
}
}
impl<T: AppImageHubTransport + ?Sized> ExternalAddProvider for AppImageHubAddProvider<'_, T> {
fn id(&self) -> &'static str {
"appimagehub"
}
fn resolve(&self, source: &SourceRef) -> Result<Option<ExternalAddResolution>, AdapterError> {
if source.kind != SourceKind::AppImageHub {
return Ok(None);
}
let adapter = AppImageHubAdapter;
let resolution = match adapter.resolve_source_with(source, self.transport)? {
AdapterResolveOutcome::Resolved(resolution) => resolution,
AdapterResolveOutcome::NoInstallableArtifact { .. } => return Ok(None),
};
let Some(resolved_item) = resolve_appimagehub_item_with(source, self.transport)
.map_err(|error| AdapterError::ResolutionFailed(format!("{error:?}")))?
else {
return Ok(None);
};
Ok(Some(ExternalAddResolution {
resolution,
selected_artifact: ArtifactCandidate {
url: resolved_item.download.url.clone(),
version: resolved_item.version.clone(),
arch: resolved_item.download.arch.clone(),
trusted_checksum: None,
weak_checksum_md5: resolved_item.download.md5sum.clone(),
selection_reason: "provider-release".to_owned(),
},
update_strategy: UpdateStrategy {
preferred: ChannelPreference {
kind: UpdateChannelKind::DirectAsset,
locator: resolved_item.download.url.clone(),
reason: "provider-release".to_owned(),
},
alternates: Vec::new(),
},
display_name_hint: Some(resolved_item.title),
}))
}
}
fn render_appimagehub_error(error: &AppImageHubError) -> String {
match error {
AppImageHubError::FixtureItemMissing(id) => {
format!("missing appimagehub fixture item {id}")
}
AppImageHubError::InsecureDownloadUrl(url) => {
format!("insecure appimagehub download url: {url}")
}
AppImageHubError::Parse(error) => error.to_string(),
AppImageHubError::Transport(error) => error.to_string(),
AppImageHubError::UnsupportedSource(locator) => {
format!("unsupported appimagehub source: {locator}")
}
}
}

View file

@ -0,0 +1,6 @@
pub mod add;
pub mod search;
pub mod source;
pub use add::{AppImageHubAdapter, AppImageHubAddProvider};
pub use search::AppImageHubSearchProvider;

View file

@ -0,0 +1,103 @@
use crate::source::appimagehub::{
AppImageHubSearchError, AppImageHubTransport, search_appimagehub_with,
};
use upm_core::app::search::{SearchProvider, SearchProviderError};
use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
pub struct AppImageHubSearchProvider<'a, T: AppImageHubTransport + ?Sized> {
transport: &'a T,
}
impl<'a, T: AppImageHubTransport + ?Sized> AppImageHubSearchProvider<'a, T> {
pub fn new(transport: &'a T) -> Self {
Self { transport }
}
}
impl<T: AppImageHubTransport + ?Sized> SearchProvider for AppImageHubSearchProvider<'_, T> {
fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
let hits = search_appimagehub_with(&query.text, query.remote_limit, self.transport)
.map_err(|error| {
SearchProviderError::new("appimagehub", &render_appimagehub_search_error(&error))
})?;
let normalized_query = normalize_lookup(&query.text);
let mut ranked_hits = hits
.into_iter()
.enumerate()
.map(|(index, hit)| {
(
appimagehub_remote_match_rank(
&normalized_query,
&hit.name,
hit.summary.as_deref(),
),
index,
hit,
)
})
.collect::<Vec<_>>();
ranked_hits.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
Ok(ranked_hits
.into_iter()
.map(|(_, _, hit)| SearchResult {
provider_id: "appimagehub".to_owned(),
display_name: hit.name,
description: hit.summary,
source_locator: hit.detail_page,
install_query: format!("appimagehub/{}", hit.id),
canonical_locator: hit.id,
version: Some(hit.version),
install_status: SearchInstallStatus::Available,
})
.collect())
}
}
fn normalize_lookup(value: &str) -> String {
value.trim().to_ascii_lowercase()
}
fn appimagehub_remote_match_rank(query: &str, name: &str, summary: Option<&str>) -> u8 {
let name = normalize_lookup(name);
let summary = summary.map(normalize_lookup);
if name == query {
return 0;
}
if name.starts_with(query) {
return 1;
}
if name.contains(query) {
return 2;
}
if summary
.as_deref()
.map(|summary| summary.starts_with(query))
.unwrap_or(false)
{
return 3;
}
if summary
.as_deref()
.map(|summary| summary.contains(query))
.unwrap_or(false)
{
return 4;
}
5
}
fn render_appimagehub_search_error(error: &AppImageHubSearchError) -> String {
match error {
AppImageHubSearchError::Parse(inner) => inner.to_string(),
AppImageHubSearchError::Transport(inner) => inner.to_string(),
}
}

View file

@ -0,0 +1,517 @@
use std::env;
use std::time::Duration;
use upm_core::domain::source::SourceRef;
const DEFAULT_APPIMAGEHUB_API_BASE: &str = "https://api.appimagehub.com/ocs/v1/content";
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
const FIXTURE_MODE_ENV: &str = "UPM_APPIMAGEHUB_FIXTURE_MODE";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppImageHubDownload {
pub url: String,
pub name: String,
pub package_type: Option<String>,
pub arch: Option<String>,
pub md5sum: Option<String>,
pub version: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppImageHubItem {
pub id: String,
pub name: String,
pub version: String,
pub summary: Option<String>,
pub detail_page: String,
pub tags: Vec<String>,
pub downloads: Vec<AppImageHubDownload>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppImageHubSearchHit {
pub id: String,
pub name: String,
pub version: String,
pub summary: Option<String>,
pub detail_page: String,
pub tags: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedAppImageHubItem {
pub source: SourceRef,
pub title: String,
pub version: String,
pub download: AppImageHubDownload,
}
pub trait AppImageHubTransport {
fn fetch_item(&self, id: &str) -> Result<AppImageHubItem, AppImageHubError>;
fn search_items(
&self,
query: &str,
limit: usize,
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError>;
}
pub fn default_transport() -> Box<dyn AppImageHubTransport> {
if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|| env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
{
Box::new(FixtureAppImageHubTransport)
} else {
Box::new(ReqwestAppImageHubTransport::new())
}
}
pub fn resolve_appimagehub_item(
source: &SourceRef,
) -> Result<Option<ResolvedAppImageHubItem>, AppImageHubError> {
let transport = default_transport();
resolve_appimagehub_item_with(source, transport.as_ref())
}
pub fn resolve_appimagehub_item_with<T: AppImageHubTransport + ?Sized>(
source: &SourceRef,
transport: &T,
) -> Result<Option<ResolvedAppImageHubItem>, AppImageHubError> {
let item = transport.fetch_item(source_id(source)?)?;
let Some(download) = item
.downloads
.iter()
.find(|download| is_appimage_download(download))
else {
return Ok(None);
};
validate_download_url(&download.url)?;
Ok(Some(ResolvedAppImageHubItem {
source: source.clone(),
title: item.name.clone(),
version: resolved_version(&item, download),
download: download.clone(),
}))
}
pub fn search_appimagehub(
query: &str,
limit: usize,
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
let transport = default_transport();
search_appimagehub_with(query, limit, transport.as_ref())
}
pub fn search_appimagehub_with<T: AppImageHubTransport + ?Sized>(
query: &str,
limit: usize,
transport: &T,
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
transport.search_items(query, limit)
}
pub struct ReqwestAppImageHubTransport {
client: reqwest::blocking::Client,
api_base: String,
}
impl Default for ReqwestAppImageHubTransport {
fn default() -> Self {
Self::new()
}
}
impl ReqwestAppImageHubTransport {
pub fn new() -> Self {
Self {
client: reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("reqwest client should build"),
api_base: env::var("UPM_APPIMAGEHUB_API_BASE")
.unwrap_or_else(|_| DEFAULT_APPIMAGEHUB_API_BASE.to_owned()),
}
}
}
impl AppImageHubTransport for ReqwestAppImageHubTransport {
fn fetch_item(&self, id: &str) -> Result<AppImageHubItem, AppImageHubError> {
let url = format!("{}/data/{id}", self.api_base);
let xml = self
.client
.get(url)
.send()
.map_err(AppImageHubError::Transport)?
.error_for_status()
.map_err(AppImageHubError::Transport)?
.text()
.map_err(AppImageHubError::Transport)?;
parse_item_xml(&xml)
}
fn search_items(
&self,
query: &str,
limit: usize,
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
let url = format!("{}/data", self.api_base);
let xml = self
.client
.get(url)
.query(&[("search", query), ("pagesize", &limit.to_string())])
.send()
.map_err(AppImageHubSearchError::Transport)?
.error_for_status()
.map_err(AppImageHubSearchError::Transport)?
.text()
.map_err(AppImageHubSearchError::Transport)?;
parse_search_xml(&xml)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct FixtureAppImageHubTransport;
impl AppImageHubTransport for FixtureAppImageHubTransport {
fn fetch_item(&self, id: &str) -> Result<AppImageHubItem, AppImageHubError> {
fixture_item(id).ok_or_else(|| AppImageHubError::FixtureItemMissing(id.to_owned()))
}
fn search_items(
&self,
query: &str,
limit: usize,
) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
Ok(fixture_search_results(query, limit))
}
}
#[derive(Debug)]
pub enum AppImageHubError {
FixtureItemMissing(String),
InsecureDownloadUrl(String),
Parse(quick_xml::DeError),
Transport(reqwest::Error),
UnsupportedSource(String),
}
#[derive(Debug)]
pub enum AppImageHubSearchError {
Parse(quick_xml::DeError),
Transport(reqwest::Error),
}
#[derive(serde::Deserialize)]
struct OcsSingleResponse {
data: OcsSingleData,
}
#[derive(serde::Deserialize)]
struct OcsSingleData {
content: OcsContent,
}
#[derive(serde::Deserialize)]
struct OcsSearchResponse {
data: OcsSearchData,
}
#[derive(serde::Deserialize)]
struct OcsSearchData {
#[serde(default)]
content: Vec<OcsContent>,
}
#[derive(serde::Deserialize)]
struct OcsContent {
id: String,
name: String,
version: Option<String>,
summary: Option<String>,
detailpage: Option<String>,
tags: Option<String>,
downloadlink1: Option<String>,
downloadname1: Option<String>,
download_package_type1: Option<String>,
download_package_arch1: Option<String>,
downloadmd5sum1: Option<String>,
download_version1: Option<String>,
downloadlink2: Option<String>,
downloadname2: Option<String>,
download_package_type2: Option<String>,
download_package_arch2: Option<String>,
downloadmd5sum2: Option<String>,
download_version2: Option<String>,
downloadlink3: Option<String>,
downloadname3: Option<String>,
download_package_type3: Option<String>,
download_package_arch3: Option<String>,
downloadmd5sum3: Option<String>,
download_version3: Option<String>,
}
fn parse_item_xml(xml: &str) -> Result<AppImageHubItem, AppImageHubError> {
let parsed =
quick_xml::de::from_str::<OcsSingleResponse>(xml).map_err(AppImageHubError::Parse)?;
Ok(content_to_item(parsed.data.content))
}
fn parse_search_xml(xml: &str) -> Result<Vec<AppImageHubSearchHit>, AppImageHubSearchError> {
if !xml.contains("<id>") {
return Ok(Vec::new());
}
let parsed =
quick_xml::de::from_str::<OcsSearchResponse>(xml).map_err(AppImageHubSearchError::Parse)?;
Ok(parsed
.data
.content
.into_iter()
.map(|content| AppImageHubSearchHit {
id: content.id,
name: content.name,
version: normalize_version_text(content.version.as_deref()),
summary: content.summary,
detail_page: content
.detailpage
.unwrap_or_else(|| "https://www.appimagehub.com".to_owned()),
tags: split_tags(content.tags.as_deref()),
})
.collect())
}
fn content_to_item(content: OcsContent) -> AppImageHubItem {
let detail_page = content
.detailpage
.clone()
.unwrap_or_else(|| "https://www.appimagehub.com".to_owned());
let summary = content.summary.clone();
let tags = split_tags(content.tags.as_deref());
let downloads = collect_downloads(&content);
AppImageHubItem {
id: content.id,
name: content.name,
version: normalize_version_text(content.version.as_deref()),
summary,
detail_page,
tags,
downloads,
}
}
fn validate_download_url(url: &str) -> Result<(), AppImageHubError> {
if !url.starts_with("https://") {
return Err(AppImageHubError::InsecureDownloadUrl(url.to_owned()));
}
Ok(())
}
fn collect_downloads(content: &OcsContent) -> Vec<AppImageHubDownload> {
let mut downloads = Vec::new();
for download in [
download_slot(
content.downloadlink1.as_deref(),
content.downloadname1.as_deref(),
content.download_package_type1.as_deref(),
content.download_package_arch1.as_deref(),
content.downloadmd5sum1.as_deref(),
content.download_version1.as_deref(),
),
download_slot(
content.downloadlink2.as_deref(),
content.downloadname2.as_deref(),
content.download_package_type2.as_deref(),
content.download_package_arch2.as_deref(),
content.downloadmd5sum2.as_deref(),
content.download_version2.as_deref(),
),
download_slot(
content.downloadlink3.as_deref(),
content.downloadname3.as_deref(),
content.download_package_type3.as_deref(),
content.download_package_arch3.as_deref(),
content.downloadmd5sum3.as_deref(),
content.download_version3.as_deref(),
),
]
.into_iter()
.flatten()
{
downloads.push(download);
}
downloads
}
fn download_slot(
link: Option<&str>,
name: Option<&str>,
package_type: Option<&str>,
arch: Option<&str>,
md5sum: Option<&str>,
version: Option<&str>,
) -> Option<AppImageHubDownload> {
let url = link?.trim();
if url.is_empty() {
return None;
}
Some(AppImageHubDownload {
url: url.to_owned(),
name: name.unwrap_or("download").trim().to_owned(),
package_type: trim_optional(package_type),
arch: trim_optional(arch),
md5sum: trim_optional(md5sum),
version: trim_optional(version),
})
}
fn trim_optional(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn normalize_version_text(value: Option<&str>) -> String {
let value = value.map(str::trim).filter(|value| !value.is_empty());
match value {
Some("Latest") | Some("latest") | None => "latest".to_owned(),
Some(other) => other.to_owned(),
}
}
fn split_tags(tags: Option<&str>) -> Vec<String> {
tags.unwrap_or_default()
.split(',')
.map(str::trim)
.filter(|tag| !tag.is_empty())
.map(ToOwned::to_owned)
.collect()
}
fn source_id(source: &SourceRef) -> Result<&str, AppImageHubError> {
source
.canonical_locator
.as_deref()
.or_else(|| source.locator.rsplit('/').next())
.filter(|value| !value.is_empty())
.ok_or_else(|| AppImageHubError::UnsupportedSource(source.locator.clone()))
}
fn is_appimage_download(download: &AppImageHubDownload) -> bool {
download
.package_type
.as_deref()
.map(|kind| kind.eq_ignore_ascii_case("appimage"))
.unwrap_or(false)
|| download.name.ends_with(".AppImage")
}
fn resolved_version(item: &AppImageHubItem, download: &AppImageHubDownload) -> String {
download
.version
.as_deref()
.map(|value| normalize_version_text(Some(value)))
.filter(|value| value != "latest")
.unwrap_or_else(|| item.version.clone())
}
fn fixture_item(id: &str) -> Option<AppImageHubItem> {
let insecure_http = env::var("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP")
.ok()
.as_deref()
== Some("1");
let bad_md5 = env::var("UPM_APPIMAGEHUB_FIXTURE_BAD_MD5").ok().as_deref() == Some("1");
match id {
"2338455" => Some(AppImageHubItem {
id: "2338455".to_owned(),
name: "Firefox by Mozilla - Official AppImage Edition".to_owned(),
version: "latest".to_owned(),
summary: Some("Take control of your internet with the Firefox browser".to_owned()),
detail_page: "https://www.appimagehub.com/p/2338455".to_owned(),
tags: vec![
"appimage".to_owned(),
"x86-64".to_owned(),
"desktop".to_owned(),
"release-stable".to_owned(),
],
downloads: vec![AppImageHubDownload {
url: if insecure_http {
"http://files06.pling.com/api/files/download/firefox-x86-64.AppImage".to_owned()
} else {
"https://files06.pling.com/api/files/download/firefox-x86-64.AppImage"
.to_owned()
},
name: "firefox-x86-64.AppImage".to_owned(),
package_type: Some("appimage".to_owned()),
arch: Some("x86-64".to_owned()),
md5sum: Some(if bad_md5 {
"00000000000000000000000000000000".to_owned()
} else {
"2a685cf45213d5a2a243273fa68dafa6".to_owned()
}),
version: None,
}],
}),
"2337998" => Some(AppImageHubItem {
id: "2337998".to_owned(),
name: "Example Non-AppImage Package".to_owned(),
version: "latest".to_owned(),
summary: Some("An item that does not expose an AppImage download".to_owned()),
detail_page: "https://www.appimagehub.com/p/2337998".to_owned(),
tags: vec!["desktop".to_owned()],
downloads: vec![AppImageHubDownload {
url: "https://files06.pling.com/api/files/download/example.deb".to_owned(),
name: "example.deb".to_owned(),
package_type: Some("debian-package".to_owned()),
arch: Some("x86-64".to_owned()),
md5sum: None,
version: Some("2.1.1".to_owned()),
}],
}),
_ => None,
}
}
fn fixture_search_results(query: &str, limit: usize) -> Vec<AppImageHubSearchHit> {
let query = query.trim().to_ascii_lowercase();
let fixtures = [
AppImageHubSearchHit {
id: "2338455".to_owned(),
name: "Firefox by Mozilla - Official AppImage Edition".to_owned(),
version: "latest".to_owned(),
summary: Some("Take control of your internet with the Firefox browser".to_owned()),
detail_page: "https://www.appimagehub.com/p/2338455".to_owned(),
tags: vec!["browser".to_owned(), "appimage".to_owned()],
},
AppImageHubSearchHit {
id: "2338484".to_owned(),
name: "Waterfox".to_owned(),
version: "latest".to_owned(),
summary: Some("Open Source, Private Browsing".to_owned()),
detail_page: "https://www.appimagehub.com/p/2338484".to_owned(),
tags: vec!["browser".to_owned(), "appimage".to_owned()],
},
];
fixtures
.into_iter()
.filter(|item| {
item.name.to_ascii_lowercase().contains(&query)
|| item
.tags
.iter()
.any(|tag| tag.to_ascii_lowercase().contains(&query))
})
.take(limit)
.collect()
}

View file

@ -0,0 +1 @@
pub mod appimagehub;

View file

@ -1,12 +1,16 @@
use aim_core::app::search::{
AppImageHubSearchProvider, GitHubSearchProvider, SearchProvider, SearchProviderError,
build_search_results_with,
use upm_appimage::add::{AppImageHubAdapter, AppImageHubAddProvider};
use upm_appimage::search::AppImageHubSearchProvider;
use upm_appimage::source::appimagehub::FixtureAppImageHubTransport;
use upm_core::adapters::traits::AdapterResolveOutcome;
use upm_core::app::providers::ExternalAddProvider;
use upm_core::app::query::resolve_query;
use upm_core::app::search::{
GitHubSearchProvider, SearchProvider, SearchProviderError, build_search_results_with,
};
use aim_core::domain::app::AppRecord;
use aim_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use aim_core::source::appimagehub::FixtureAppImageHubTransport;
use aim_core::source::github::FixtureGitHubTransport;
use upm_core::domain::app::AppRecord;
use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use upm_core::source::github::FixtureGitHubTransport;
struct StubProvider {
hit: SearchResult,
@ -106,3 +110,40 @@ fn search_can_merge_github_and_appimagehub_providers() {
.any(|hit| hit.provider_id == "appimagehub")
);
}
#[test]
fn appimagehub_adapter_resolves_installable_items_through_fixture_transport() {
let adapter = AppImageHubAdapter;
let source = resolve_query("appimagehub/2338455").unwrap();
let resolution = adapter
.resolve_source_with(&source, &FixtureAppImageHubTransport)
.unwrap();
assert!(matches!(
resolution,
AdapterResolveOutcome::Resolved(resolution)
if resolution.source.kind == SourceKind::AppImageHub
&& resolution.source.canonical_locator.as_deref() == Some("2338455")
&& resolution.release.version == "latest"
));
}
#[test]
fn appimagehub_add_provider_resolves_external_add_plan() {
let provider = AppImageHubAddProvider::new(&FixtureAppImageHubTransport);
let source = resolve_query("appimagehub/2338455").unwrap();
let resolution = provider.resolve(&source).unwrap().unwrap();
assert_eq!(resolution.resolution.source.kind, SourceKind::AppImageHub);
assert_eq!(resolution.resolution.release.version, "latest");
assert_eq!(
resolution.selected_artifact.url,
"https://files06.pling.com/api/files/download/firefox-x86-64.AppImage"
);
assert_eq!(
resolution.display_name_hint.as_deref(),
Some("Firefox by Mozilla - Official AppImage Edition")
);
}

View file

@ -1,5 +1,5 @@
[package]
name = "aim-core"
name = "upm-core"
version.workspace = true
edition.workspace = true
license.workspace = true

View file

@ -1,4 +1,3 @@
pub mod appimagehub;
pub mod direct_url;
pub mod github;
pub mod gitlab;
@ -11,14 +10,7 @@ use crate::adapters::traits::SourceAdapter;
use crate::domain::source::SourceRef;
pub fn all_adapter_kinds() -> Vec<&'static str> {
vec![
"appimagehub",
"github",
"gitlab",
"direct-url",
"zsync",
"sourceforge",
]
vec!["github", "gitlab", "direct-url", "zsync", "sourceforge"]
}
pub fn supports_source<A: SourceAdapter + ?Sized>(adapter: &A, source: &SourceRef) -> bool {

View file

@ -3,7 +3,6 @@ use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use crate::adapters::appimagehub::AppImageHubAdapter;
use crate::adapters::direct_url::DirectUrlAdapter;
use crate::adapters::gitlab::GitLabAdapter;
use crate::adapters::sourceforge::SourceForgeAdapter;
@ -14,6 +13,7 @@ use crate::app::interaction::{InteractionKind, InteractionRequest};
use crate::app::progress::{
NoopReporter, OperationEvent, OperationKind, OperationStage, ProgressReporter,
};
use crate::app::providers::{ExternalAddResolution, ProviderRegistry};
use crate::app::query::{ResolveQueryError, resolve_query};
use crate::app::scope::{ScopeOverride, resolve_install_scope_with_default};
use crate::domain::app::{AppRecord, InstallMetadata, InstallScope};
@ -25,14 +25,14 @@ use crate::integration::install::{
use crate::integration::policy::{IntegrationMode, resolve_install_policy};
use crate::metadata::parse_document;
use crate::platform::probe_live_host;
use crate::source::appimagehub::resolve_appimagehub_item;
use crate::source::github::{
GitHubDiscoveryError, GitHubTransport, discover_github_candidates_with, http_client_policy,
};
use crate::update::channels::build_channels;
use crate::update::ranking::{rank_channels, select_artifact, to_preference};
const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE";
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
const FIXTURE_MODE_ENV: &str = "UPM_GITHUB_FIXTURE_MODE";
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct AddSecurityPolicy {
@ -42,11 +42,12 @@ pub struct AddSecurityPolicy {
pub fn build_add_plan(query: &str) -> Result<AddPlan, BuildAddPlanError> {
let transport = crate::source::github::default_transport();
let mut reporter = NoopReporter;
build_add_plan_with_reporter_and_policy(
build_add_plan_with_reporter_and_policy_and_registry(
query,
transport.as_ref(),
&mut reporter,
AddSecurityPolicy::default(),
&ProviderRegistry::default(),
)
}
@ -55,11 +56,12 @@ pub fn build_add_plan_with<T: GitHubTransport + ?Sized>(
transport: &T,
) -> Result<AddPlan, BuildAddPlanError> {
let mut reporter = NoopReporter;
build_add_plan_with_reporter_and_policy(
build_add_plan_with_reporter_and_policy_and_registry(
query,
transport,
&mut reporter,
AddSecurityPolicy::default(),
&ProviderRegistry::default(),
)
}
@ -68,11 +70,40 @@ pub fn build_add_plan_with_reporter<T: GitHubTransport + ?Sized>(
transport: &T,
reporter: &mut impl ProgressReporter,
) -> Result<AddPlan, BuildAddPlanError> {
build_add_plan_with_reporter_and_policy(
build_add_plan_with_reporter_and_policy_and_registry(
query,
transport,
reporter,
AddSecurityPolicy::default(),
&ProviderRegistry::default(),
)
}
pub fn build_add_plan_with_registered_providers<T: GitHubTransport + ?Sized>(
query: &str,
transport: &T,
providers: &ProviderRegistry<'_>,
policy: AddSecurityPolicy,
) -> Result<AddPlan, BuildAddPlanError> {
let mut reporter = NoopReporter;
build_add_plan_with_reporter_and_policy_and_registry(
query,
transport,
&mut reporter,
policy,
providers,
)
}
pub fn build_add_plan_with_reporter_and_registered_providers<T: GitHubTransport + ?Sized>(
query: &str,
transport: &T,
reporter: &mut impl ProgressReporter,
providers: &ProviderRegistry<'_>,
policy: AddSecurityPolicy,
) -> Result<AddPlan, BuildAddPlanError> {
build_add_plan_with_reporter_and_policy_and_registry(
query, transport, reporter, policy, providers,
)
}
@ -81,6 +112,22 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
transport: &T,
reporter: &mut impl ProgressReporter,
policy: AddSecurityPolicy,
) -> Result<AddPlan, BuildAddPlanError> {
build_add_plan_with_reporter_and_policy_and_registry(
query,
transport,
reporter,
policy,
&ProviderRegistry::default(),
)
}
fn build_add_plan_with_reporter_and_policy_and_registry<T: GitHubTransport + ?Sized>(
query: &str,
transport: &T,
reporter: &mut impl ProgressReporter,
policy: AddSecurityPolicy,
providers: &ProviderRegistry<'_>,
) -> Result<AddPlan, BuildAddPlanError> {
reporter.report(&OperationEvent::StageChanged {
stage: OperationStage::ResolveQuery,
@ -91,8 +138,7 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
let mut interactions = Vec::new();
let mut parsed_metadata = Vec::new();
let mut display_name_hint = None;
let (resolution, selected_artifact, update_strategy) = match source.kind {
let (resolution, selected_artifact, update_strategy, display_name_hint) = match source.kind {
SourceKind::GitHub => {
reporter.report(&OperationEvent::StageChanged {
stage: OperationStage::DiscoverRelease,
@ -148,6 +194,7 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
},
artifact,
strategy,
None,
)
}
SourceKind::GitLab => {
@ -188,59 +235,29 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
selection_reason: "provider-release".to_owned(),
};
(resolution, artifact, strategy)
(resolution, artifact, strategy, None)
}
SourceKind::AppImageHub => {
reporter.report(&OperationEvent::StageChanged {
stage: OperationStage::DiscoverRelease,
message: "discovering release".to_owned(),
});
let adapter = AppImageHubAdapter;
let resolution = match adapter
.resolve_source(&source)
.map_err(|error| BuildAddPlanError::Adapter("appimagehub", error))?
if let Some(external_resolution) =
resolve_registered_external_add_provider(&source, providers)?
{
AdapterResolveOutcome::Resolved(resolution) => resolution,
AdapterResolveOutcome::NoInstallableArtifact { source } => {
return Err(BuildAddPlanError::NoInstallableArtifact { source });
}
};
let resolved_item = resolve_appimagehub_item(&resolution.source)
.map_err(|error| {
BuildAddPlanError::Adapter(
"appimagehub",
crate::adapters::traits::AdapterError::ResolutionFailed(format!(
"{error:?}"
)),
)
})?
.ok_or(BuildAddPlanError::NoInstallableArtifact {
source: resolution.source.clone(),
})?;
display_name_hint = Some(resolved_item.title.clone());
reporter.report(&OperationEvent::StageChanged {
stage: OperationStage::SelectArtifact,
message: "selecting artifact".to_owned(),
});
let artifact = ArtifactCandidate {
url: resolved_item.download.url.clone(),
version: resolved_item.version.clone(),
arch: resolved_item.download.arch.clone(),
trusted_checksum: None,
weak_checksum_md5: resolved_item.download.md5sum.clone(),
selection_reason: "provider-release".to_owned(),
};
let strategy = UpdateStrategy {
preferred: crate::domain::update::ChannelPreference {
kind: crate::domain::update::UpdateChannelKind::DirectAsset,
locator: resolved_item.download.url.clone(),
reason: "provider-release".to_owned(),
},
alternates: Vec::new(),
};
(resolution, artifact, strategy)
reporter.report(&OperationEvent::StageChanged {
stage: OperationStage::SelectArtifact,
message: "selecting artifact".to_owned(),
});
(
external_resolution.resolution,
external_resolution.selected_artifact,
external_resolution.update_strategy,
external_resolution.display_name_hint,
)
} else {
return Err(BuildAddPlanError::NoInstallableArtifact { source });
}
}
SourceKind::DirectUrl => {
reporter.report(&OperationEvent::StageChanged {
@ -274,7 +291,7 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
alternates: Vec::new(),
};
(resolution, artifact, strategy)
(resolution, artifact, strategy, None)
}
SourceKind::SourceForge => {
reporter.report(&OperationEvent::StageChanged {
@ -315,7 +332,7 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
alternates: Vec::new(),
};
(resolution, artifact, strategy)
(resolution, artifact, strategy, None)
}
_ => {
reporter.report(&OperationEvent::StageChanged {
@ -345,7 +362,7 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
},
alternates: Vec::new(),
};
(resolution, artifact, strategy)
(resolution, artifact, strategy, None)
}
};
@ -359,6 +376,21 @@ pub fn build_add_plan_with_reporter_and_policy<T: GitHubTransport + ?Sized>(
})
}
fn resolve_registered_external_add_provider(
source: &crate::domain::source::SourceRef,
providers: &ProviderRegistry<'_>,
) -> Result<Option<ExternalAddResolution>, BuildAddPlanError> {
for provider in &providers.external_add_providers {
match provider.resolve(source) {
Ok(Some(resolution)) => return Ok(Some(resolution)),
Ok(None) => continue,
Err(error) => return Err(BuildAddPlanError::Adapter(provider.id(), error)),
}
}
Ok(None)
}
pub fn prefer_latest_tracking(mut plan: AddPlan) -> AddPlan {
if let Some(index) = plan
.update_strategy
@ -465,7 +497,7 @@ pub fn install_app_with_reporter(
install_home,
&policy
.desktop_entry_root
.join(format!("aim-{}.desktop", record.stable_id)),
.join(format!("upm-{}.desktop", record.stable_id)),
);
let icon_path = resolve_target_path(
install_home,
@ -475,7 +507,7 @@ pub fn install_app_with_reporter(
stage: OperationStage::DownloadArtifact,
message: "downloading artifact".to_owned(),
});
let staging_root = install_home.join(".local/share/aim/staging");
let staging_root = install_home.join(".local/share/upm/staging");
let staged_payload_path = staged_appimage_path(&staging_root, &record.stable_id);
let artifact_size_bytes = download_artifact_to_staged_path_with_reporter(
&plan.selected_artifact.url,
@ -630,7 +662,9 @@ fn download_artifact_to_staged_path_with_reporter(
) -> Result<u64, InstallAppError> {
let policy = http_client_policy();
if env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") {
if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|| env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
{
let bytes = b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82";
return download_to_staged_path_with_retries(staged_payload_path, reporter, policy, || {
Ok((

View file

@ -3,6 +3,7 @@ pub mod identity;
pub mod interaction;
pub mod list;
pub mod progress;
pub mod providers;
pub mod query;
pub mod remove;
pub mod scope;

View file

@ -0,0 +1,24 @@
use crate::adapters::traits::{AdapterError, AdapterResolution};
use crate::app::search::SearchProvider;
use crate::domain::source::SourceRef;
use crate::domain::update::{ArtifactCandidate, UpdateStrategy};
pub trait ExternalAddProvider {
fn id(&self) -> &'static str;
fn resolve(&self, source: &SourceRef) -> Result<Option<ExternalAddResolution>, AdapterError>;
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ExternalAddResolution {
pub resolution: AdapterResolution,
pub selected_artifact: ArtifactCandidate,
pub update_strategy: UpdateStrategy,
pub display_name_hint: Option<String>,
}
#[derive(Default)]
pub struct ProviderRegistry<'a> {
pub search_providers: Vec<&'a dyn SearchProvider>,
pub external_add_providers: Vec<&'a dyn ExternalAddProvider>,
}

View file

@ -1,11 +1,9 @@
use crate::app::providers::ProviderRegistry;
use crate::domain::app::AppRecord;
use crate::domain::search::{
InstalledSearchMatch, SearchInstallStatus, SearchQuery, SearchResult, SearchResults,
SearchWarning,
};
use crate::source::appimagehub::{
AppImageHubSearchError, AppImageHubTransport, search_appimagehub_with,
};
use crate::source::github::{
GitHubSearchError, GitHubTransport, TransportRelease, default_transport,
search_github_repositories_with,
@ -40,17 +38,26 @@ pub fn build_search_results(
query: &SearchQuery,
installed_apps: &[AppRecord],
) -> Result<SearchResults, SearchError> {
let github_transport = default_transport();
let appimagehub_transport = crate::source::appimagehub::default_transport();
let github_provider = GitHubSearchProvider::new(github_transport.as_ref());
let appimagehub_provider = AppImageHubSearchProvider::new(appimagehub_transport.as_ref());
build_search_results_with(
build_search_results_with_registered_providers(
query,
installed_apps,
&[&github_provider, &appimagehub_provider],
&ProviderRegistry::default(),
)
}
pub fn build_search_results_with_registered_providers(
query: &SearchQuery,
installed_apps: &[AppRecord],
providers: &ProviderRegistry<'_>,
) -> Result<SearchResults, SearchError> {
let github_transport = default_transport();
let github_provider = GitHubSearchProvider::new(github_transport.as_ref());
let mut resolved_providers = vec![&github_provider as &dyn SearchProvider];
resolved_providers.extend(providers.search_providers.iter().copied());
build_search_results_with(query, installed_apps, &resolved_providers)
}
pub fn build_search_results_with(
query: &SearchQuery,
installed_apps: &[AppRecord],
@ -94,58 +101,6 @@ impl<'a, T: GitHubTransport + ?Sized> GitHubSearchProvider<'a, T> {
}
}
pub struct AppImageHubSearchProvider<'a, T: AppImageHubTransport + ?Sized> {
transport: &'a T,
}
impl<'a, T: AppImageHubTransport + ?Sized> AppImageHubSearchProvider<'a, T> {
pub fn new(transport: &'a T) -> Self {
Self { transport }
}
}
impl<T: AppImageHubTransport + ?Sized> SearchProvider for AppImageHubSearchProvider<'_, T> {
fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
let hits = search_appimagehub_with(&query.text, query.remote_limit, self.transport)
.map_err(|error| {
SearchProviderError::new("appimagehub", &render_appimagehub_search_error(&error))
})?;
let normalized_query = normalize_lookup(&query.text);
let mut ranked_hits = hits
.into_iter()
.enumerate()
.map(|(index, hit)| {
(
appimagehub_remote_match_rank(
&normalized_query,
&hit.name,
hit.summary.as_deref(),
),
index,
hit,
)
})
.collect::<Vec<_>>();
ranked_hits.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1)));
Ok(ranked_hits
.into_iter()
.map(|(_, _, hit)| SearchResult {
provider_id: "appimagehub".to_owned(),
display_name: hit.name,
description: hit.summary,
source_locator: hit.detail_page,
install_query: format!("appimagehub/{}", hit.id),
canonical_locator: hit.id,
version: Some(hit.version),
install_status: SearchInstallStatus::Available,
})
.collect())
}
}
impl<T: GitHubTransport + ?Sized> SearchProvider for GitHubSearchProvider<'_, T> {
fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
let name_only_query = format!("{} in:name", query.text);
@ -392,45 +347,3 @@ fn render_github_search_error(error: &GitHubSearchError) -> String {
GitHubSearchError::Transport(inner) => inner.to_string(),
}
}
fn appimagehub_remote_match_rank(query: &str, name: &str, summary: Option<&str>) -> u8 {
let name = normalize_lookup(name);
let summary = summary.map(normalize_lookup);
if name == query {
return 0;
}
if name.starts_with(query) {
return 1;
}
if name.contains(query) {
return 2;
}
if summary
.as_deref()
.map(|summary| summary.starts_with(query))
.unwrap_or(false)
{
return 3;
}
if summary
.as_deref()
.map(|summary| summary.contains(query))
.unwrap_or(false)
{
return 4;
}
5
}
fn render_appimagehub_search_error(error: &AppImageHubSearchError) -> String {
match error {
AppImageHubSearchError::Parse(inner) => inner.to_string(),
AppImageHubSearchError::Transport(inner) => inner.to_string(),
}
}

View file

@ -291,7 +291,7 @@ fn stage_existing_installation(
}
let stage_dir = install_home
.join(".local/share/aim/rollback")
.join(".local/share/upm/rollback")
.join(&app.stable_id);
fs::create_dir_all(&stage_dir)
.map_err(|error| format!("failed to create rollback staging directory: {error}"))?;

View file

@ -11,7 +11,7 @@ pub fn managed_appimage_path(home_dir: &Path, scope: InstallScope, app_id: &str)
}
pub fn desktop_entry_path(home_dir: &Path, scope: InstallScope, app_id: &str) -> PathBuf {
scope_applications_dir(home_dir, scope).join(format!("aim-{app_id}.desktop"))
scope_applications_dir(home_dir, scope).join(format!("upm-{app_id}.desktop"))
}
pub fn icon_path(home_dir: &Path, scope: InstallScope, app_id: &str) -> PathBuf {

View file

@ -37,7 +37,7 @@ pub fn resolve_install_policy(
(DistroFamily::Immutable, InstallScope::System) if capabilities.is_immutable => {
Ok(InstallPolicy {
scope: InstallScope::User,
payload_root: PathBuf::from(".local/lib/aim/appimages"),
payload_root: PathBuf::from(".local/lib/upm/appimages"),
desktop_entry_root: PathBuf::from(".local/share/applications"),
icon_root: PathBuf::from(".local/share/icons/hicolor/256x256/apps"),
integration_mode: IntegrationMode::Degraded,
@ -57,7 +57,7 @@ pub fn resolve_install_policy(
}),
_ => Ok(InstallPolicy {
scope: InstallScope::User,
payload_root: PathBuf::from(".local/lib/aim/appimages"),
payload_root: PathBuf::from(".local/lib/upm/appimages"),
desktop_entry_root: PathBuf::from(".local/share/applications"),
icon_root: PathBuf::from(".local/share/icons/hicolor/256x256/apps"),
integration_mode: if capabilities.has_desktop_session {

View file

@ -62,7 +62,7 @@ fn icon_theme_root(icon_path: &Path) -> PathBuf {
}
fn audit_helper(helper: &Path, args: &[&Path]) {
if env::var("AIM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
if env::var("UPM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
return;
}
@ -72,7 +72,7 @@ fn audit_helper(helper: &Path, args: &[&Path]) {
.collect::<Vec<_>>()
.join(" ");
eprintln!(
"[aim] helper exec: {}{}{}",
"[upm] helper exec: {}{}{}",
helper.display(),
if rendered_args.is_empty() { "" } else { " " },
rendered_args
@ -80,23 +80,23 @@ fn audit_helper(helper: &Path, args: &[&Path]) {
}
fn audit_helper_status(helper: &Path, code: Option<i32>) {
if env::var("AIM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
if env::var("UPM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
return;
}
match code {
Some(code) => eprintln!("[aim] helper exit: {} code={code}", helper.display()),
Some(code) => eprintln!("[upm] helper exit: {} code={code}", helper.display()),
None => eprintln!(
"[aim] helper exit: {} terminated by signal",
"[upm] helper exit: {} terminated by signal",
helper.display()
),
}
}
fn audit_helper_failure(helper: &Path, error: &str) {
if env::var("AIM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
if env::var("UPM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") {
return;
}
eprintln!("[aim] helper failure: {} error={error}", helper.display());
eprintln!("[upm] helper failure: {} error={error}", helper.display());
}

View file

@ -7,3 +7,5 @@ pub mod platform;
pub mod registry;
pub mod source;
pub mod update;
pub use app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry};

View file

@ -71,7 +71,7 @@ fn is_writable_dir(path: &Path) -> bool {
return false;
}
let probe_path = path.join(".aim-write-test");
let probe_path = path.join(".upm-write-test");
let result = OpenOptions::new()
.create(true)
.write(true)

View file

@ -10,11 +10,11 @@ pub use crate::domain::app::InstallScope;
pub use capabilities::{DesktopHelpers, HostCapabilities, WritableRoots};
pub use distro::{DistroFamily, detect_distro_family};
const OS_RELEASE_PATH_ENV: &str = "AIM_OS_RELEASE_PATH";
const HELPER_PATHS_ENV: &str = "AIM_HELPER_PATHS";
const OS_RELEASE_PATH_ENV: &str = "UPM_OS_RELEASE_PATH";
const HELPER_PATHS_ENV: &str = "UPM_HELPER_PATHS";
pub fn user_managed_appimages_dir(home_dir: &Path) -> PathBuf {
home_dir.join(".local/lib/aim/appimages")
home_dir.join(".local/lib/upm/appimages")
}
pub fn user_applications_dir(home_dir: &Path) -> PathBuf {
@ -26,7 +26,7 @@ pub fn user_icons_dir(home_dir: &Path) -> PathBuf {
}
pub fn system_managed_appimages_dir() -> PathBuf {
PathBuf::from("/opt/aim/appimages")
PathBuf::from("/opt/upm/appimages")
}
pub fn system_applications_dir() -> PathBuf {

View file

@ -4,7 +4,8 @@ use std::time::Duration;
use crate::domain::source::SourceRef;
const DEFAULT_APPIMAGEHUB_API_BASE: &str = "https://api.appimagehub.com/ocs/v1/content";
const FIXTURE_MODE_ENV: &str = "AIM_APPIMAGEHUB_FIXTURE_MODE";
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
const FIXTURE_MODE_ENV: &str = "UPM_APPIMAGEHUB_FIXTURE_MODE";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AppImageHubDownload {
@ -56,9 +57,8 @@ pub trait AppImageHubTransport {
}
pub fn default_transport() -> Box<dyn AppImageHubTransport> {
if env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|| env::var("AIM_GITHUB_FIXTURE_MODE").ok().as_deref() == Some("1")
{
if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|| env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") {
Box::new(FixtureAppImageHubTransport)
} else {
Box::new(ReqwestAppImageHubTransport::new())
@ -129,7 +129,7 @@ impl ReqwestAppImageHubTransport {
.timeout(Duration::from_secs(30))
.build()
.expect("reqwest client should build"),
api_base: env::var("AIM_APPIMAGEHUB_API_BASE")
api_base: env::var("UPM_APPIMAGEHUB_API_BASE")
.unwrap_or_else(|_| DEFAULT_APPIMAGEHUB_API_BASE.to_owned()),
}
}
@ -424,11 +424,11 @@ fn resolved_version(item: &AppImageHubItem, download: &AppImageHubDownload) -> S
}
fn fixture_item(id: &str) -> Option<AppImageHubItem> {
let insecure_http = env::var("AIM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP")
let insecure_http = env::var("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP")
.ok()
.as_deref()
== Some("1");
let bad_md5 = env::var("AIM_APPIMAGEHUB_FIXTURE_BAD_MD5").ok().as_deref() == Some("1");
let bad_md5 = env::var("UPM_APPIMAGEHUB_FIXTURE_BAD_MD5").ok().as_deref() == Some("1");
match id {
"2338455" => Some(AppImageHubItem {

View file

@ -5,7 +5,8 @@ use crate::domain::source::{ResolvedRelease, SourceRef};
use crate::metadata::MetadataDocument;
const DEFAULT_GITHUB_API_BASE: &str = "https://api.github.com";
const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE";
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
const FIXTURE_MODE_ENV: &str = "UPM_GITHUB_FIXTURE_MODE";
const DEFAULT_HTTP_TIMEOUT_SECS: u64 = 30;
const DEFAULT_HTTP_MAX_RETRIES: usize = 3;
@ -176,7 +177,9 @@ pub fn search_github_repositories_with<T: GitHubTransport + ?Sized>(
}
pub fn default_transport() -> Box<dyn GitHubTransport> {
if env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") {
if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
|| env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1")
{
Box::new(FixtureGitHubTransport)
} else {
Box::new(ReqwestGitHubTransport::new())
@ -200,13 +203,13 @@ impl ReqwestGitHubTransport {
let mut default_headers = reqwest::header::HeaderMap::new();
default_headers.insert(
reqwest::header::USER_AGENT,
reqwest::header::HeaderValue::from_static("aim/0.1"),
reqwest::header::HeaderValue::from_static("upm/0.1"),
);
default_headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/vnd.github+json"),
);
if let Some(token) = env::var("AIM_GITHUB_TOKEN")
if let Some(token) = env::var("UPM_GITHUB_TOKEN")
.ok()
.or_else(|| env::var("GITHUB_TOKEN").ok())
&& let Ok(value) = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
@ -220,7 +223,7 @@ impl ReqwestGitHubTransport {
.timeout(policy.timeout)
.build()
.expect("reqwest client should build"),
api_base: env::var("AIM_GITHUB_API_BASE")
api_base: env::var("UPM_GITHUB_API_BASE")
.unwrap_or_else(|_| DEFAULT_GITHUB_API_BASE.to_owned()),
}
}

View file

@ -1,3 +1,2 @@
pub mod appimagehub;
pub mod github;
pub mod input;

View file

@ -1,16 +1,14 @@
use aim_core::adapters::appimagehub::AppImageHubAdapter;
use aim_core::adapters::direct_url::DirectUrlAdapter;
use aim_core::adapters::github::GitHubAdapter;
use aim_core::adapters::gitlab::GitLabAdapter;
use aim_core::adapters::sourceforge::SourceForgeAdapter;
use aim_core::adapters::traits::{
use upm_core::adapters::direct_url::DirectUrlAdapter;
use upm_core::adapters::github::GitHubAdapter;
use upm_core::adapters::gitlab::GitLabAdapter;
use upm_core::adapters::sourceforge::SourceForgeAdapter;
use upm_core::adapters::traits::{
AdapterCapabilities, AdapterError, AdapterResolution, AdapterResolveOutcome, SourceAdapter,
};
use aim_core::app::query::resolve_query;
use aim_core::domain::source::{
use upm_core::app::query::resolve_query;
use upm_core::domain::source::{
NormalizedSourceKind, ResolvedRelease, SourceInputKind, SourceKind, SourceRef,
};
use aim_core::source::appimagehub::FixtureAppImageHubTransport;
struct FileArtifactAdapter;
@ -61,60 +59,6 @@ fn adapter_capabilities_can_report_exact_resolution_only() {
assert!(!capabilities.supports_search);
}
#[test]
fn appimagehub_adapter_reports_search_and_exact_resolution_capabilities() {
let adapter = AppImageHubAdapter;
assert_eq!(adapter.id(), "appimagehub");
assert_eq!(
adapter.repository_source_kind(),
Some(SourceKind::AppImageHub)
);
assert_eq!(adapter.exact_source_kind(), None);
assert_eq!(
adapter.capabilities(),
AdapterCapabilities {
supports_search: true,
supports_exact_resolution: true,
}
);
}
#[test]
fn appimagehub_adapter_resolves_installable_items_through_fixture_transport() {
let adapter = AppImageHubAdapter;
let source = resolve_query("appimagehub/2338455").unwrap();
let resolution = adapter
.resolve_source_with(&source, &FixtureAppImageHubTransport)
.unwrap();
assert!(matches!(
resolution,
AdapterResolveOutcome::Resolved(AdapterResolution {
source,
release: ResolvedRelease { version, .. },
}) if source.kind == SourceKind::AppImageHub
&& source.canonical_locator.as_deref() == Some("2338455")
&& version == "latest"
));
}
#[test]
fn appimagehub_adapter_reports_no_installable_artifact_for_non_appimage_items() {
let adapter = AppImageHubAdapter;
let source = resolve_query("appimagehub/2337998").unwrap();
let resolution = adapter
.resolve_source_with(&source, &FixtureAppImageHubTransport)
.unwrap();
assert_eq!(
resolution,
AdapterResolveOutcome::NoInstallableArtifact { source }
);
}
#[test]
fn repository_backed_resolvers_accept_only_their_own_source_kind() {
let github_source = resolve_query("sharkdp/bat").unwrap();

View file

@ -1,14 +1,14 @@
use aim_core::adapters::all_adapter_kinds;
use upm_core::adapters::all_adapter_kinds;
#[test]
fn all_expected_adapter_kinds_are_registered() {
let kinds = all_adapter_kinds();
assert!(kinds.contains(&"appimagehub"));
assert!(kinds.contains(&"github"));
assert!(kinds.contains(&"gitlab"));
assert!(kinds.contains(&"direct-url"));
assert!(kinds.contains(&"zsync"));
assert!(kinds.contains(&"sourceforge"));
assert!(!kinds.contains(&"appimagehub"));
assert!(!kinds.contains(&"custom-json"));
}

View file

@ -1,8 +1,8 @@
use std::fs;
use aim_core::integration::install::{InstallRequest, PayloadInstallError, execute_install};
use aim_core::platform::DesktopHelpers;
use tempfile::tempdir;
use upm_core::integration::install::{InstallRequest, PayloadInstallError, execute_install};
use upm_core::platform::DesktopHelpers;
const VALID_FIXTURE_SHA512: &str =
"ZZma4ZD+9XB4GGTHCNZu8I92OY02YrEvIG89ZtRNi99W8SZKwWkmGZz/QyNBxqAt0XeiKtcR80/dMnKlwpcIWw==";

View file

@ -2,15 +2,15 @@ use std::fs;
use std::io::{self, Cursor, Read};
use std::time::Duration;
use aim_core::app::add::{
use tempfile::tempdir;
use upm_core::app::add::{
InstallAppError, download_to_staged_path_with_retries,
stream_payload_to_staged_file_with_reporter,
};
use aim_core::app::progress::{NoopReporter, OperationEvent};
use aim_core::integration::install::{InstallRequest, execute_install};
use aim_core::platform::DesktopHelpers;
use aim_core::source::github::HttpClientPolicy;
use tempfile::tempdir;
use upm_core::app::progress::{NoopReporter, OperationEvent};
use upm_core::integration::install::{InstallRequest, execute_install};
use upm_core::platform::DesktopHelpers;
use upm_core::source::github::HttpClientPolicy;
#[test]
fn payload_streaming_writes_staged_file_and_reports_progress() {

View file

@ -1,6 +1,6 @@
use aim_core::app::add::{build_add_plan_with, materialize_app_record, prefer_latest_tracking};
use aim_core::app::query::resolve_query;
use aim_core::source::github::FixtureGitHubTransport;
use upm_core::app::add::{build_add_plan_with, materialize_app_record, prefer_latest_tracking};
use upm_core::app::query::resolve_query;
use upm_core::source::github::FixtureGitHubTransport;
#[test]
fn github_adapter_can_normalize_owner_repo_source() {

View file

@ -1,8 +1,8 @@
use aim_core::app::query::resolve_query;
use aim_core::source::github::{
use std::time::Duration;
use upm_core::app::query::resolve_query;
use upm_core::source::github::{
FixtureGitHubTransport, discover_github_candidates_with, http_client_policy,
};
use std::time::Duration;
#[test]
fn discovery_reports_appimage_assets_and_latest_linux_yml() {

View file

@ -1,5 +1,5 @@
use aim_core::app::identity::{IdentityFallback, resolve_identity};
use aim_core::domain::app::IdentityConfidence;
use upm_core::app::identity::{IdentityFallback, resolve_identity};
use upm_core::domain::app::IdentityConfidence;
#[test]
fn unresolved_identity_can_fall_back_to_url() {
@ -42,6 +42,6 @@ fn identifiers_containing_dot_dot_are_rejected() {
assert_eq!(
error,
aim_core::app::identity::ResolveIdentityError::InvalidStableId
upm_core::app::identity::ResolveIdentityError::InvalidStableId
);
}

View file

@ -1,15 +1,15 @@
use aim_core::app::add::{BuildAddPlanError, build_add_plan_with};
use aim_core::app::query::ResolveQueryError;
use aim_core::app::update::execute_updates;
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use aim_core::domain::source::SourceKind;
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceRef};
use aim_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
use aim_core::platform::DesktopHelpers;
use aim_core::source::github::FixtureGitHubTransport;
use std::fs;
use std::sync::Mutex;
use tempfile::tempdir;
use upm_core::app::add::{BuildAddPlanError, build_add_plan_with};
use upm_core::app::query::ResolveQueryError;
use upm_core::app::update::execute_updates;
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use upm_core::domain::source::SourceKind;
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceRef};
use upm_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
use upm_core::platform::DesktopHelpers;
use upm_core::source::github::FixtureGitHubTransport;
static ENV_LOCK: Mutex<()> = Mutex::new(());
@ -27,7 +27,7 @@ fn integration_failure_removes_new_payload_and_generated_files() {
fs::write(&staged_path, b"\x7fELFAppImage").unwrap();
let final_payload_path = payload_root.join("bat.AppImage");
let desktop_entry_path = blocking_path.join("aim-bat.desktop");
let desktop_entry_path = blocking_path.join("upm-bat.desktop");
let error = execute_install(&InstallRequest {
staged_payload_path: &staged_path,
final_payload_path: &final_payload_path,
@ -85,13 +85,13 @@ fn failed_update_restores_tracked_desktop_and_icon_files() {
let root = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("DISPLAY", ":99");
std::env::set_var("XDG_CURRENT_DESKTOP", "test");
}
let payload_path = root.path().join("tracked/team-app.AppImage");
let desktop_path = root.path().join("tracked/aim-team-app.desktop");
let desktop_path = root.path().join("tracked/upm-team-app.desktop");
let icon_path = root.path().join("tracked/team-app.png");
fs::create_dir_all(payload_path.parent().unwrap()).unwrap();
fs::write(&payload_path, b"previous-payload").unwrap();

View file

@ -1,13 +1,13 @@
use aim_core::app::add::{build_add_plan_with_reporter, install_app_with_reporter};
use aim_core::app::progress::{OperationEvent, OperationStage};
use aim_core::domain::app::InstallScope;
use aim_core::domain::source::{NormalizedSourceKind, SourceKind};
use aim_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
use aim_core::platform::DesktopHelpers;
use aim_core::source::github::FixtureGitHubTransport;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
use upm_core::app::add::{build_add_plan_with_reporter, install_app_with_reporter};
use upm_core::app::progress::{OperationEvent, OperationStage};
use upm_core::domain::app::InstallScope;
use upm_core::domain::source::{NormalizedSourceKind, SourceKind};
use upm_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install};
use upm_core::platform::DesktopHelpers;
use upm_core::source::github::FixtureGitHubTransport;
fn write_staged_payload(root: &std::path::Path, name: &str, bytes: &[u8]) -> std::path::PathBuf {
let staged_path = root.join("staging").join(format!("{name}.download"));
@ -32,7 +32,7 @@ fn install_writes_desktop_entry_and_reports_refresh_warning_only() {
trusted_checksum: None,
weak_checksum_md5: None,
desktop: Some(DesktopIntegrationRequest {
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
desktop_entry_path: &desktop_root.join("upm-bat.desktop"),
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
icon_path: None,
icon_bytes: None,
@ -86,7 +86,7 @@ fn install_executes_refresh_helpers_when_available() {
trusted_checksum: None,
weak_checksum_md5: None,
desktop: Some(DesktopIntegrationRequest {
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
desktop_entry_path: &desktop_root.join("upm-bat.desktop"),
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
icon_path: Some(&icon_root.join("bat.png")),
icon_bytes: None,
@ -128,7 +128,7 @@ fn install_extracts_icon_from_appimage_payload_when_icon_path_is_requested() {
trusted_checksum: None,
weak_checksum_md5: None,
desktop: Some(DesktopIntegrationRequest {
desktop_entry_path: &desktop_root.join("aim-bat.desktop"),
desktop_entry_path: &desktop_root.join("upm-bat.desktop"),
desktop_entry_contents: "[Desktop Entry]\nName=bat\nExec=bat.AppImage\nType=Application\n",
icon_path: Some(&icon_root.join("bat.png")),
icon_bytes: None,
@ -152,7 +152,7 @@ fn install_app_reports_operation_stages_in_order() {
let mut events: Vec<OperationEvent> = Vec::new();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let mut reporter = |event: &OperationEvent| events.push(event.clone());
@ -249,7 +249,7 @@ fn install_app_sanitizes_desktop_entry_display_names() {
let mut reporter = Vec::new();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let mut capture = |event: &OperationEvent| reporter.push(event.clone());
@ -349,7 +349,7 @@ fn gitlab_install_preserves_truthful_gitlab_origin() {
let root = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let mut reporter = |_event: &OperationEvent| {};
@ -399,7 +399,7 @@ fn direct_url_install_preserves_truthful_direct_url_origin() {
let root = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let mut reporter = |_event: &OperationEvent| {};
@ -484,7 +484,7 @@ fn sourceforge_latest_download_install_preserves_truthful_origin() {
let root = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let mut reporter = |_event: &OperationEvent| {};
@ -514,7 +514,7 @@ fn sourceforge_release_folder_install_preserves_truthful_origin() {
let root = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let mut reporter = |_event: &OperationEvent| {};
@ -577,7 +577,7 @@ fn sourceforge_file_like_release_download_install_preserves_input_but_stores_rel
let root = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let mut reporter = |_event: &OperationEvent| {};

View file

@ -0,0 +1,28 @@
use std::path::Path;
use upm_core::domain::app::InstallScope;
use upm_core::integration::paths::{desktop_entry_path, managed_appimage_path};
#[test]
fn user_scope_path_lands_under_home_managed_dir() {
let path = managed_appimage_path(Path::new("/home/test"), InstallScope::User, "bat");
assert_eq!(
path,
Path::new("/home/test/.local/lib/upm/appimages/bat.AppImage")
);
}
#[test]
fn system_scope_path_lands_under_opt_upm_dir() {
let path = managed_appimage_path(Path::new("/home/test"), InstallScope::System, "bat");
assert_eq!(path, Path::new("/opt/upm/appimages/bat.AppImage"));
}
#[test]
fn system_scope_desktop_entry_uses_upm_prefix() {
let path = desktop_entry_path(Path::new("/home/test"), InstallScope::System, "bat");
assert_eq!(path, Path::new("/usr/share/applications/upm-bat.desktop"));
}

View file

@ -1,7 +1,7 @@
use aim_core::integration::install::stage_and_commit_payload;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
use upm_core::integration::install::stage_and_commit_payload;
#[test]
fn payload_commit_moves_staged_appimage_into_final_location() {

View file

@ -1,6 +1,6 @@
use aim_core::integration::policy::{IntegrationMode, resolve_install_policy};
use aim_core::platform::{DistroFamily, HostCapabilities, InstallScope};
use std::path::Path;
use upm_core::integration::policy::{IntegrationMode, resolve_install_policy};
use upm_core::platform::{DistroFamily, HostCapabilities, InstallScope};
#[test]
fn immutable_system_request_downgrades_to_user_when_allowed() {
@ -36,7 +36,7 @@ fn system_policy_uses_managed_payload_and_native_integration_roots() {
.unwrap();
assert_eq!(policy.scope, InstallScope::System);
assert_eq!(policy.payload_root, Path::new("/opt/aim/appimages"));
assert_eq!(policy.payload_root, Path::new("/opt/upm/appimages"));
assert_eq!(
policy.desktop_entry_root,
Path::new("/usr/share/applications")

View file

@ -1,5 +1,5 @@
use aim_core::app::scope::{ScopeOverride, resolve_install_scope};
use aim_core::domain::app::InstallScope;
use upm_core::app::scope::{ScopeOverride, resolve_install_scope};
use upm_core::domain::app::InstallScope;
#[test]
fn explicit_scope_override_beats_effective_user() {

View file

@ -1,5 +1,5 @@
use aim_core::domain::update::ParsedMetadataKind;
use aim_core::metadata::{MetadataDocument, parse_document};
use upm_core::domain::update::ParsedMetadataKind;
use upm_core::metadata::{MetadataDocument, parse_document};
#[test]
fn unknown_document_returns_typed_warning_not_panic() {

View file

@ -1,5 +1,5 @@
use aim_core::domain::update::ParsedMetadataKind;
use aim_core::metadata::{MetadataDocument, parse_document};
use upm_core::domain::update::ParsedMetadataKind;
use upm_core::metadata::{MetadataDocument, parse_document};
#[test]
fn parses_latest_linux_yml_into_download_hints() {

View file

@ -1,5 +1,5 @@
use aim_core::domain::update::ParsedMetadataKind;
use aim_core::metadata::{MetadataDocument, parse_document};
use upm_core::domain::update::ParsedMetadataKind;
use upm_core::metadata::{MetadataDocument, parse_document};
#[test]
fn parses_zsync_document_into_channel_hints() {

View file

@ -1,8 +1,8 @@
use aim_core::platform::capabilities::{probe_desktop_helpers, probe_writable_roots};
use aim_core::platform::distro::{DistroFamily, detect_distro_family};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
use upm_core::platform::capabilities::{probe_desktop_helpers, probe_writable_roots};
use upm_core::platform::distro::{DistroFamily, detect_distro_family};
#[test]
fn detects_fedora_family_from_os_release() {

View file

@ -0,0 +1,159 @@
use upm_core::app::add::{AddSecurityPolicy, build_add_plan_with_registered_providers};
use upm_core::app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry};
use upm_core::app::search::{SearchProvider, build_search_results_with_registered_providers};
use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
use upm_core::domain::source::{
NormalizedSourceKind, ResolvedRelease, SourceInputKind, SourceKind, SourceRef,
};
use upm_core::domain::update::{
ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy,
};
use upm_core::source::github::FixtureGitHubTransport;
struct StubSearchProvider;
impl SearchProvider for StubSearchProvider {
fn search(
&self,
_query: &SearchQuery,
) -> Result<Vec<SearchResult>, upm_core::app::search::SearchProviderError> {
Ok(vec![SearchResult {
provider_id: "external-search".to_owned(),
display_name: "Firefox Nightly".to_owned(),
description: Some("Provided by external registry".to_owned()),
source_locator: "https://example.invalid/firefox-nightly".to_owned(),
install_query: "external/firefox-nightly".to_owned(),
canonical_locator: "external/firefox-nightly".to_owned(),
version: Some("2026.03.21".to_owned()),
install_status: SearchInstallStatus::Available,
}])
}
}
struct StubExternalAddProvider;
impl ExternalAddProvider for StubExternalAddProvider {
fn id(&self) -> &'static str {
"stub-appimage"
}
fn resolve(
&self,
source: &SourceRef,
) -> Result<Option<ExternalAddResolution>, upm_core::adapters::traits::AdapterError> {
Ok(
(source.kind == SourceKind::AppImageHub).then(|| ExternalAddResolution {
resolution: upm_core::adapters::traits::AdapterResolution {
source: SourceRef {
kind: SourceKind::AppImageHub,
locator: source.locator.clone(),
input_kind: SourceInputKind::AppImageHubShorthand,
normalized_kind: NormalizedSourceKind::AppImageHub,
canonical_locator: Some("2338455".to_owned()),
requested_tag: None,
requested_asset_name: None,
tracks_latest: true,
},
release: ResolvedRelease {
version: "stable".to_owned(),
prerelease: false,
},
},
selected_artifact: ArtifactCandidate {
url: "https://downloads.example.invalid/firefox.AppImage".to_owned(),
version: "stable".to_owned(),
arch: Some("x86_64".to_owned()),
trusted_checksum: None,
weak_checksum_md5: Some("deadbeef".to_owned()),
selection_reason: "provider-release".to_owned(),
},
update_strategy: UpdateStrategy {
preferred: ChannelPreference {
kind: UpdateChannelKind::DirectAsset,
locator: "https://downloads.example.invalid/firefox.AppImage".to_owned(),
reason: "provider-release".to_owned(),
},
alternates: Vec::new(),
},
display_name_hint: Some(
"Firefox by Mozilla - Official AppImage Edition".to_owned(),
),
}),
)
}
}
#[test]
fn build_search_results_with_registered_providers_uses_external_hits() {
let query = SearchQuery::new("firefox");
let search_provider = StubSearchProvider;
let providers = ProviderRegistry {
search_providers: vec![&search_provider],
external_add_providers: Vec::new(),
};
let results = build_search_results_with_registered_providers(&query, &[], &providers).unwrap();
let external_hit = results
.remote_hits
.iter()
.find(|hit| hit.provider_id == "external-search")
.unwrap();
assert_eq!(external_hit.install_query, "external/firefox-nightly");
assert!(
results
.remote_hits
.iter()
.all(|hit| hit.provider_id != "appimagehub")
);
}
#[test]
fn build_add_plan_with_registered_providers_requires_external_provider_for_appimagehub() {
let registry = ProviderRegistry::default();
let error = build_add_plan_with_registered_providers(
"appimagehub/2338455",
&FixtureGitHubTransport,
&registry,
AddSecurityPolicy::default(),
)
.unwrap_err();
assert!(matches!(
error,
upm_core::app::add::BuildAddPlanError::NoInstallableArtifact { .. }
));
}
#[test]
fn build_add_plan_with_registered_providers_delegates_appimagehub_like_sources() {
let provider = StubExternalAddProvider;
let registry = ProviderRegistry {
search_providers: Vec::new(),
external_add_providers: vec![&provider],
};
let plan = build_add_plan_with_registered_providers(
"appimagehub/2338455",
&FixtureGitHubTransport,
&registry,
AddSecurityPolicy::default(),
)
.unwrap();
assert_eq!(plan.resolution.source.kind, SourceKind::AppImageHub);
assert_eq!(
plan.resolution.source.canonical_locator.as_deref(),
Some("2338455")
);
assert_eq!(
plan.selected_artifact.url,
"https://downloads.example.invalid/firefox.AppImage"
);
assert_eq!(
plan.display_name_hint.as_deref(),
Some("Firefox by Mozilla - Official AppImage Edition")
);
}

View file

@ -1,5 +1,5 @@
use aim_core::app::query::resolve_query;
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind};
use upm_core::app::query::resolve_query;
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind};
#[test]
fn owner_repo_defaults_to_github() {
@ -233,21 +233,21 @@ fn classifies_single_segment_sourceforge_release_download_with_query_as_candidat
fn rejects_malformed_gitlab_url() {
let error = resolve_query("https://gitlab.com/example").unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]
fn rejects_unsupported_gitlab_url_shape() {
let error = resolve_query("https://gitlab.com/example/team-app/-/issues").unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]
fn rejects_unsupported_gitlab_nested_resource_url() {
let error = resolve_query("https://gitlab.com/example/team-app/issues").unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]
@ -255,14 +255,14 @@ fn rejects_unsupported_gitlab_release_permalink_url() {
let error = resolve_query("https://gitlab.com/example/team-app/-/releases/permalink/latest")
.unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]
fn rejects_unsupported_gitlab_issue_detail_url() {
let error = resolve_query("https://gitlab.com/example/team-app/issues/1").unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]
@ -270,7 +270,7 @@ fn rejects_unsupported_gitlab_blob_url() {
let error =
resolve_query("https://gitlab.com/example/team-app/blob/main/README.md").unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]
@ -291,28 +291,28 @@ fn classifies_ambiguous_gitlab_deep_reserved_segment_as_candidate() {
fn rejects_unsupported_gitlab_packages_url() {
let error = resolve_query("https://gitlab.com/example/team-app/packages").unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]
fn rejects_malformed_sourceforge_url() {
let error = resolve_query("https://sourceforge.net/projects/").unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]
fn rejects_malformed_appimagehub_shorthand() {
let error = resolve_query("appimagehub/firefox").unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]
fn rejects_unsupported_sourceforge_url_shape() {
let error = resolve_query("https://sourceforge.net/projects/team-app/rss").unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]
@ -335,7 +335,7 @@ fn rejects_unsupported_sourceforge_folder_download_shape() {
let error = resolve_query("https://sourceforge.net/projects/team-app/files/releases/download")
.unwrap_err();
assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported);
assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported);
}
#[test]

View file

@ -1,5 +1,5 @@
use aim_core::registry::store::RegistryStore;
use tempfile::tempdir;
use upm_core::registry::store::RegistryStore;
#[test]
fn registry_round_trips_app_records() {
@ -13,28 +13,28 @@ fn registry_round_trips_app_records() {
fn registry_round_trips_update_strategy_and_alternates() {
let dir = tempdir().unwrap();
let store = RegistryStore::new(dir.path().join("registry.toml"));
let registry = aim_core::registry::model::Registry {
let registry = upm_core::registry::model::Registry {
version: 1,
apps: vec![aim_core::domain::app::AppRecord {
apps: vec![upm_core::domain::app::AppRecord {
stable_id: "t3code".to_owned(),
display_name: "T3 Code".to_owned(),
source_input: Some("pingdotgg/t3code".to_owned()),
source: None,
installed_version: Some("0.0.11".to_owned()),
update_strategy: Some(aim_core::domain::update::UpdateStrategy {
preferred: aim_core::domain::update::ChannelPreference {
kind: aim_core::domain::update::UpdateChannelKind::DirectAsset,
update_strategy: Some(upm_core::domain::update::UpdateStrategy {
preferred: upm_core::domain::update::ChannelPreference {
kind: upm_core::domain::update::UpdateChannelKind::DirectAsset,
locator: "https://example.test/app.AppImage".to_owned(),
reason: "install-origin-match".to_owned(),
},
alternates: vec![
aim_core::domain::update::ChannelPreference {
kind: aim_core::domain::update::UpdateChannelKind::GitHubReleases,
upm_core::domain::update::ChannelPreference {
kind: upm_core::domain::update::UpdateChannelKind::GitHubReleases,
locator: "pingdotgg/t3code".to_owned(),
reason: "heuristic-match".to_owned(),
},
aim_core::domain::update::ChannelPreference {
kind: aim_core::domain::update::UpdateChannelKind::ElectronBuilder,
upm_core::domain::update::ChannelPreference {
kind: upm_core::domain::update::UpdateChannelKind::ElectronBuilder,
locator: "https://example.test/latest-linux.yml".to_owned(),
reason: "metadata-guided".to_owned(),
},
@ -57,9 +57,9 @@ fn registry_round_trips_update_strategy_and_alternates() {
fn registry_round_trips_install_metadata() {
let dir = tempdir().unwrap();
let store = RegistryStore::new(dir.path().join("registry.toml"));
let registry = aim_core::registry::model::Registry {
let registry = upm_core::registry::model::Registry {
version: 1,
apps: vec![aim_core::domain::app::AppRecord {
apps: vec![upm_core::domain::app::AppRecord {
stable_id: "t3code".to_owned(),
display_name: "T3 Code".to_owned(),
source_input: Some("pingdotgg/t3code".to_owned()),
@ -67,13 +67,13 @@ fn registry_round_trips_install_metadata() {
installed_version: Some("0.0.11".to_owned()),
update_strategy: None,
metadata: Vec::new(),
install: Some(aim_core::domain::app::InstallMetadata {
scope: aim_core::domain::app::InstallScope::User,
install: Some(upm_core::domain::app::InstallMetadata {
scope: upm_core::domain::app::InstallScope::User,
payload_path: Some(
"/tmp/install-home/.local/lib/aim/appimages/t3code.AppImage".to_owned(),
"/tmp/install-home/.local/lib/upm/appimages/t3code.AppImage".to_owned(),
),
desktop_entry_path: Some(
"/tmp/install-home/.local/share/applications/aim-t3code.desktop".to_owned(),
"/tmp/install-home/.local/share/applications/upm-t3code.desktop".to_owned(),
),
icon_path: Some(
"/tmp/install-home/.local/share/icons/hicolor/256x256/apps/t3code.png"
@ -87,14 +87,14 @@ fn registry_round_trips_install_metadata() {
let loaded = store.load().unwrap();
let install = loaded.apps[0].install.as_ref().unwrap();
assert_eq!(install.scope, aim_core::domain::app::InstallScope::User);
assert_eq!(install.scope, upm_core::domain::app::InstallScope::User);
assert_eq!(
install.payload_path.as_deref(),
Some("/tmp/install-home/.local/lib/aim/appimages/t3code.AppImage")
Some("/tmp/install-home/.local/lib/upm/appimages/t3code.AppImage")
);
assert_eq!(
install.desktop_entry_path.as_deref(),
Some("/tmp/install-home/.local/share/applications/aim-t3code.desktop")
Some("/tmp/install-home/.local/share/applications/upm-t3code.desktop")
);
assert_eq!(
install.icon_path.as_deref(),
@ -106,18 +106,18 @@ fn registry_round_trips_install_metadata() {
fn registry_round_trips_source_identity_for_new_provider_kinds() {
let dir = tempdir().unwrap();
let store = RegistryStore::new(dir.path().join("registry.toml"));
let registry = aim_core::registry::model::Registry {
let registry = upm_core::registry::model::Registry {
version: 1,
apps: vec![
aim_core::domain::app::AppRecord {
upm_core::domain::app::AppRecord {
stable_id: "example-team-app".to_owned(),
display_name: "team-app".to_owned(),
source_input: Some("https://gitlab.com/example/team-app".to_owned()),
source: Some(aim_core::domain::source::SourceRef {
kind: aim_core::domain::source::SourceKind::GitLab,
source: Some(upm_core::domain::source::SourceRef {
kind: upm_core::domain::source::SourceKind::GitLab,
locator: "https://gitlab.com/example/team-app".to_owned(),
input_kind: aim_core::domain::source::SourceInputKind::GitLabUrl,
normalized_kind: aim_core::domain::source::NormalizedSourceKind::GitLab,
input_kind: upm_core::domain::source::SourceInputKind::GitLabUrl,
normalized_kind: upm_core::domain::source::NormalizedSourceKind::GitLab,
canonical_locator: Some("example/team-app".to_owned()),
requested_tag: None,
requested_asset_name: None,
@ -128,18 +128,18 @@ fn registry_round_trips_source_identity_for_new_provider_kinds() {
metadata: Vec::new(),
install: None,
},
aim_core::domain::app::AppRecord {
upm_core::domain::app::AppRecord {
stable_id: "team-app".to_owned(),
display_name: "team-app".to_owned(),
source_input: Some(
"https://sourceforge.net/projects/team-app/files/latest/download".to_owned(),
),
source: Some(aim_core::domain::source::SourceRef {
kind: aim_core::domain::source::SourceKind::SourceForge,
source: Some(upm_core::domain::source::SourceRef {
kind: upm_core::domain::source::SourceKind::SourceForge,
locator: "https://sourceforge.net/projects/team-app/files/latest/download"
.to_owned(),
input_kind: aim_core::domain::source::SourceInputKind::SourceForgeUrl,
normalized_kind: aim_core::domain::source::NormalizedSourceKind::SourceForge,
input_kind: upm_core::domain::source::SourceInputKind::SourceForgeUrl,
normalized_kind: upm_core::domain::source::NormalizedSourceKind::SourceForge,
canonical_locator: Some("team-app".to_owned()),
requested_tag: None,
requested_asset_name: None,
@ -150,15 +150,15 @@ fn registry_round_trips_source_identity_for_new_provider_kinds() {
metadata: Vec::new(),
install: None,
},
aim_core::domain::app::AppRecord {
upm_core::domain::app::AppRecord {
stable_id: "url-example.com-downloads-team-app.appimage".to_owned(),
display_name: "https://example.com/downloads/team-app.AppImage".to_owned(),
source_input: Some("https://example.com/downloads/team-app.AppImage".to_owned()),
source: Some(aim_core::domain::source::SourceRef {
kind: aim_core::domain::source::SourceKind::DirectUrl,
source: Some(upm_core::domain::source::SourceRef {
kind: upm_core::domain::source::SourceKind::DirectUrl,
locator: "https://example.com/downloads/team-app.AppImage".to_owned(),
input_kind: aim_core::domain::source::SourceInputKind::DirectUrl,
normalized_kind: aim_core::domain::source::NormalizedSourceKind::DirectUrl,
input_kind: upm_core::domain::source::SourceInputKind::DirectUrl,
normalized_kind: upm_core::domain::source::NormalizedSourceKind::DirectUrl,
canonical_locator: None,
requested_tag: None,
requested_asset_name: None,
@ -213,9 +213,9 @@ fn registry_save_is_atomic_and_cleans_up_temp_file() {
let store = RegistryStore::new(registry_path.clone());
store
.save(&aim_core::registry::model::Registry {
.save(&upm_core::registry::model::Registry {
version: 1,
apps: vec![aim_core::domain::app::AppRecord {
apps: vec![upm_core::domain::app::AppRecord {
stable_id: "bat".to_owned(),
display_name: "Bat".to_owned(),
source_input: None,
@ -242,7 +242,7 @@ fn registry_exclusive_lock_rejects_second_mutator() {
assert!(matches!(
error,
aim_core::registry::store::RegistryStoreError::LockUnavailable
upm_core::registry::store::RegistryStoreError::LockUnavailable
));
}
@ -251,9 +251,9 @@ fn registry_mutate_exclusive_reloads_and_writes_latest_state() {
let dir = tempdir().unwrap();
let store = RegistryStore::new(dir.path().join("registry.toml"));
store
.save(&aim_core::registry::model::Registry {
.save(&upm_core::registry::model::Registry {
version: 1,
apps: vec![aim_core::domain::app::AppRecord {
apps: vec![upm_core::domain::app::AppRecord {
stable_id: "bat".to_owned(),
display_name: "Bat".to_owned(),
source_input: None,
@ -268,7 +268,7 @@ fn registry_mutate_exclusive_reloads_and_writes_latest_state() {
store
.mutate_exclusive(|registry| {
registry.apps.push(aim_core::domain::app::AppRecord {
registry.apps.push(upm_core::domain::app::AppRecord {
stable_id: "t3code".to_owned(),
display_name: "T3 Code".to_owned(),
source_input: None,

View file

@ -1,13 +1,13 @@
use aim_core::app::interaction::{InteractionKind, InteractionRequest};
use aim_core::app::list::build_list_rows;
use aim_core::app::progress::{OperationEvent, OperationStage};
use aim_core::app::remove::{
build_removal_plan, remove_registered_app_with_reporter, resolve_registered_app,
};
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use std::path::Path;
use tempfile::tempdir;
use upm_core::app::interaction::{InteractionKind, InteractionRequest};
use upm_core::app::list::build_list_rows;
use upm_core::app::progress::{OperationEvent, OperationStage};
use upm_core::app::remove::{
build_removal_plan, remove_registered_app_with_reporter, resolve_registered_app,
};
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
#[test]
fn remove_flow_rejects_unknown_app_names() {
@ -74,7 +74,7 @@ fn ambiguous_remove_matches_include_stable_ids_for_client_choice() {
assert_eq!(
error,
aim_core::app::remove::ResolveRegisteredAppError::Ambiguous {
upm_core::app::remove::ResolveRegisteredAppError::Ambiguous {
request: InteractionRequest {
key: "select-registered-app".to_owned(),
kind: InteractionKind::SelectRegisteredApp {
@ -98,8 +98,8 @@ fn removal_plan_prefers_persisted_install_metadata_paths() {
metadata: Vec::new(),
install: Some(InstallMetadata {
scope: InstallScope::System,
payload_path: Some("/opt/aim/appimages/bat.AppImage".to_owned()),
desktop_entry_path: Some("/usr/share/applications/aim-bat.desktop".to_owned()),
payload_path: Some("/opt/upm/appimages/bat.AppImage".to_owned()),
desktop_entry_path: Some("/usr/share/applications/upm-bat.desktop".to_owned()),
icon_path: Some("/usr/share/icons/hicolor/256x256/apps/bat.png".to_owned()),
}),
};
@ -110,8 +110,8 @@ fn removal_plan_prefers_persisted_install_metadata_paths() {
assert_eq!(
plan.artifact_paths,
vec![
"/opt/aim/appimages/bat.AppImage".to_owned(),
"/usr/share/applications/aim-bat.desktop".to_owned(),
"/opt/upm/appimages/bat.AppImage".to_owned(),
"/usr/share/applications/upm-bat.desktop".to_owned(),
"/usr/share/icons/hicolor/256x256/apps/bat.png".to_owned(),
]
);
@ -135,8 +135,8 @@ fn removal_plan_falls_back_to_derived_managed_user_paths() {
assert_eq!(
plan.artifact_paths,
vec![
"/home/test/.local/lib/aim/appimages/bat.AppImage".to_owned(),
"/home/test/.local/share/applications/aim-bat.desktop".to_owned(),
"/home/test/.local/lib/upm/appimages/bat.AppImage".to_owned(),
"/home/test/.local/share/applications/upm-bat.desktop".to_owned(),
"/home/test/.local/share/icons/hicolor/256x256/apps/bat.png".to_owned(),
]
);
@ -158,7 +158,7 @@ fn remove_flow_reports_resolution_and_cleanup_events() {
payload_path: Some(
install_home
.path()
.join(".local/lib/aim/appimages/bat.AppImage")
.join(".local/lib/upm/appimages/bat.AppImage")
.display()
.to_string(),
),

View file

@ -1,10 +1,10 @@
use aim_core::app::search::{
use upm_core::app::search::{
GitHubSearchProvider, SearchProvider, SearchProviderError, build_search_results_with,
};
use aim_core::domain::app::AppRecord;
use aim_core::domain::search::{SearchInstallStatus, SearchQuery};
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use aim_core::source::github::{FixtureGitHubTransport, search_github_repositories_with};
use upm_core::domain::app::AppRecord;
use upm_core::domain::search::{SearchInstallStatus, SearchQuery};
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use upm_core::source::github::{FixtureGitHubTransport, search_github_repositories_with};
#[test]
fn github_fixtures_return_normalized_remote_hits() {
@ -206,7 +206,7 @@ impl SearchProvider for FailingProvider {
fn search(
&self,
_query: &SearchQuery,
) -> Result<Vec<aim_core::domain::search::SearchResult>, SearchProviderError> {
) -> Result<Vec<upm_core::domain::search::SearchResult>, SearchProviderError> {
Err(SearchProviderError::new("github", "fixture rate limit"))
}
}

View file

@ -1,12 +1,12 @@
use aim_core::app::show::{build_show_result, build_show_result_with};
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use aim_core::domain::show::{ShowResult, ShowResultError};
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use aim_core::domain::update::{
use upm_core::app::show::{build_show_result, build_show_result_with};
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use upm_core::domain::show::{ShowResult, ShowResultError};
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use upm_core::domain::update::{
ChannelPreference, MetadataHints, ParsedMetadata, ParsedMetadataKind, UpdateChannelKind,
UpdateStrategy,
};
use aim_core::source::github::FixtureGitHubTransport;
use upm_core::source::github::FixtureGitHubTransport;
#[test]
fn exact_installed_match_returns_installed_details() {
@ -48,8 +48,8 @@ fn exact_installed_match_returns_installed_details() {
install: Some(InstallMetadata {
scope: InstallScope::User,
payload_path: Some("/tmp/bat.AppImage".to_owned()),
desktop_entry_path: Some("/tmp/aim-bat.desktop".to_owned()),
icon_path: Some("/tmp/aim-bat.png".to_owned()),
desktop_entry_path: Some("/tmp/upm-bat.desktop".to_owned()),
icon_path: Some("/tmp/upm-bat.png".to_owned()),
}),
}];
@ -180,7 +180,7 @@ fn remote_show_projects_tracking_preference_interaction() {
ShowResult::Remote(remote) => {
assert!(remote.interactions.iter().any(|interaction| matches!(
interaction,
aim_core::domain::show::RemoteInteractionSummary::ChooseTrackingPreference { .. }
upm_core::domain::show::RemoteInteractionSummary::ChooseTrackingPreference { .. }
)));
}
other => panic!("expected remote result, got {other:?}"),

View file

@ -1,16 +1,16 @@
use aim_core::app::add::AddSecurityPolicy;
use aim_core::app::progress::{NoopReporter, OperationEvent, OperationStage};
use aim_core::app::update::{
build_update_plan, execute_updates, execute_updates_with_reporter,
execute_updates_with_reporter_and_policy,
};
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use aim_core::domain::update::{ChannelPreference, UpdateChannelKind, UpdateStrategy};
use aim_core::integration::paths::managed_appimage_path;
use std::fs;
use std::sync::Mutex;
use tempfile::tempdir;
use upm_core::app::add::AddSecurityPolicy;
use upm_core::app::progress::{NoopReporter, OperationEvent, OperationStage};
use upm_core::app::update::{
build_update_plan, execute_updates, execute_updates_with_reporter,
execute_updates_with_reporter_and_policy,
};
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use upm_core::domain::update::{ChannelPreference, UpdateChannelKind, UpdateStrategy};
use upm_core::integration::paths::managed_appimage_path;
static ENV_LOCK: Mutex<()> = Mutex::new(());
@ -189,7 +189,7 @@ fn update_execution_rebuilds_gitlab_source_without_rewriting_origin() {
let install_home = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let previous = AppRecord {
@ -253,7 +253,7 @@ fn update_execution_rebuilds_sourceforge_release_folder_without_rewriting_origin
let install_home = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let previous = AppRecord {
@ -323,7 +323,7 @@ fn direct_http_updates_are_rejected_by_default() {
let install_home = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let previous = AppRecord {
@ -357,7 +357,7 @@ fn direct_http_updates_are_rejected_by_default() {
assert_eq!(result.failed_count(), 1);
assert!(matches!(
&result.items[0].status,
aim_core::domain::update::UpdateExecutionStatus::Failed { reason }
upm_core::domain::update::UpdateExecutionStatus::Failed { reason }
if reason.contains("InsecureHttpSource")
));
}
@ -370,7 +370,7 @@ fn direct_http_updates_can_be_allowed_by_policy() {
let install_home = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let previous = AppRecord {
@ -417,7 +417,7 @@ fn update_execution_uses_stored_sourceforge_releases_root_for_file_like_inputs()
let install_home = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
}
let previous = AppRecord {
@ -482,7 +482,7 @@ fn failed_update_restores_previous_payload_contents() {
let install_home = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("DISPLAY", ":99");
std::env::set_var("XDG_CURRENT_DESKTOP", "test");
}
@ -536,7 +536,7 @@ fn successful_update_removes_rollback_staging_directory() {
let install_home = tempdir().unwrap();
unsafe {
std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1");
std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1");
std::env::remove_var("DISPLAY");
std::env::remove_var("WAYLAND_DISPLAY");
std::env::remove_var("XDG_CURRENT_DESKTOP");
@ -578,7 +578,7 @@ fn successful_update_removes_rollback_staging_directory() {
assert!(
!install_home
.path()
.join(".local/share/aim/rollback")
.join(".local/share/upm/rollback")
.exists()
);
}

View file

@ -1,5 +1,5 @@
[package]
name = "aim-cli"
name = "upm"
version.workspace = true
edition.workspace = true
license.workspace = true
@ -8,7 +8,7 @@ license.workspace = true
path = "src/lib.rs"
[[bin]]
name = "aim"
name = "upm"
path = "src/main.rs"
[dependencies]
@ -21,7 +21,8 @@ libc.workspace = true
ratatui.workspace = true
serde.workspace = true
toml.workspace = true
aim-core = { path = "../aim-core" }
upm-appimage = { path = "../upm-appimage" }
upm-core = { path = "../upm-core" }
[dev-dependencies]
assert_cmd.workspace = true

View file

@ -1,8 +1,8 @@
use clap::Parser;
#[derive(Debug, Parser)]
#[command(name = "aim")]
#[command(about = "AppImage Manager")]
#[command(name = "upm")]
#[command(about = "Universal Package Manager")]
pub struct Cli {
#[arg(global = true, long = "system", conflicts_with = "user")]
pub system: bool,

View file

@ -52,10 +52,10 @@ struct FileThemeConfig {
impl AppConfig {
pub fn load() -> LoadedConfig {
let system_path = Some(PathBuf::from("/etc/aim/config.toml"));
let system_path = Some(PathBuf::from("/etc/upm/config.toml"));
let user_path = env::var_os("HOME")
.map(PathBuf::from)
.map(|home| home.join(".config/aim/config.toml"));
.map(|home| home.join(".config/upm/config.toml"));
Self::load_from_paths(system_path.as_deref(), user_path.as_deref())
}

View file

@ -68,16 +68,16 @@ pub fn load_from_path(path: &Path) -> Result<CliConfig, ConfigError> {
}
pub fn default_path() -> PathBuf {
if let Some(path) = env::var_os("AIM_CONFIG_PATH") {
if let Some(path) = env::var_os("UPM_CONFIG_PATH") {
return PathBuf::from(path);
}
if let Some(config_home) = env::var_os("XDG_CONFIG_HOME") {
return PathBuf::from(config_home).join("aim/config.toml");
return PathBuf::from(config_home).join("upm/config.toml");
}
let home = env::var_os("HOME").unwrap_or_else(|| ".".into());
PathBuf::from(home).join(".config/aim/config.toml")
PathBuf::from(home).join(".config/upm/config.toml")
}
#[derive(Debug)]

View file

@ -1,28 +1,30 @@
pub mod cli;
pub mod config;
pub mod providers;
pub mod ui;
use std::collections::{HashMap, HashSet};
use std::env;
use std::path::{Path, PathBuf};
use aim_core::app::add::{
AddPlan, AddSecurityPolicy, InstalledApp, build_add_plan_with_reporter_and_policy,
install_app_with_reporter, resolve_requested_scope,
use upm_core::app::add::{
AddPlan, AddSecurityPolicy, InstalledApp,
build_add_plan_with_reporter_and_registered_providers, install_app_with_reporter,
resolve_requested_scope,
};
use aim_core::app::list::{ListRow, build_list_rows};
use aim_core::app::progress::{
use upm_core::app::list::{ListRow, build_list_rows};
use upm_core::app::progress::{
NoopReporter, OperationEvent, OperationKind, OperationStage, ProgressReporter,
};
use aim_core::app::remove::{RemovalResult, remove_registered_app_with_reporter};
use aim_core::app::search::build_search_results;
use aim_core::app::show::{build_installed_show_results, build_show_result};
use aim_core::app::update::{build_update_plan, execute_updates_with_reporter_and_policy};
use aim_core::domain::app::AppRecord;
use aim_core::domain::search::{SearchQuery, SearchResults};
use aim_core::domain::show::{InstalledShow, ShowResult};
use aim_core::domain::update::{UpdateExecutionResult, UpdatePlan};
use aim_core::registry::store::RegistryStore;
use upm_core::app::remove::{RemovalResult, remove_registered_app_with_reporter};
use upm_core::app::search::build_search_results_with_registered_providers;
use upm_core::app::show::{build_installed_show_results, build_show_result};
use upm_core::app::update::{build_update_plan, execute_updates_with_reporter_and_policy};
use upm_core::domain::app::AppRecord;
use upm_core::domain::search::{SearchQuery, SearchResults};
use upm_core::domain::show::{InstalledShow, ShowResult};
use upm_core::domain::update::{UpdateExecutionResult, UpdatePlan};
use upm_core::registry::store::RegistryStore;
pub use cli::args::Cli;
@ -80,7 +82,13 @@ pub fn dispatch_with_reporter_and_config(
kind: OperationKind::Search,
label: query.clone(),
});
let results = build_search_results(&SearchQuery::new(&query), &apps)?;
let results = providers::with_provider_registry(|providers| {
build_search_results_with_registered_providers(
&SearchQuery::new(&query),
&apps,
providers,
)
})?;
reporter.report(&OperationEvent::Finished {
summary: format!("search complete: {} remote hits", results.remote_hits.len()),
});
@ -123,28 +131,37 @@ pub fn dispatch_with_reporter_and_config(
if let Some(query) = cli.query {
let requested_scope = resolve_requested_scope(cli.system, cli.user, is_effective_root());
let transport = aim_core::source::github::default_transport();
let plan_result = build_add_plan_with_reporter_and_policy(
&query,
transport.as_ref(),
reporter,
AddSecurityPolicy {
allow_http_user_sources: config.allow_http,
},
);
let transport = upm_core::source::github::default_transport();
let plan_result = providers::with_provider_registry(|providers| {
build_add_plan_with_reporter_and_registered_providers(
&query,
transport.as_ref(),
reporter,
providers,
AddSecurityPolicy {
allow_http_user_sources: config.allow_http,
},
)
});
let mut plan = match plan_result {
Ok(plan) => plan,
Err(
aim_core::app::add::BuildAddPlanError::Query(
aim_core::app::query::ResolveQueryError::Unsupported,
upm_core::app::add::BuildAddPlanError::Query(
upm_core::app::query::ResolveQueryError::Unsupported,
)
| aim_core::app::add::BuildAddPlanError::NoInstallableArtifact { .. },
| upm_core::app::add::BuildAddPlanError::NoInstallableArtifact { .. },
) => {
reporter.report(&OperationEvent::Started {
kind: OperationKind::Search,
label: query.clone(),
});
let results = build_search_results(&SearchQuery::new(&query), &apps)?;
let results = providers::with_provider_registry(|providers| {
build_search_results_with_registered_providers(
&SearchQuery::new(&query),
&apps,
providers,
)
})?;
reporter.report(&OperationEvent::Finished {
summary: format!("search complete: {} remote hits", results.remote_hits.len()),
});
@ -188,13 +205,17 @@ pub fn render_with_config(result: &DispatchResult, config: &config::CliConfig) -
ui::render::render_dispatch_result_with_config(result, config)
}
fn registry_path() -> PathBuf {
if let Some(path) = env::var_os("AIM_REGISTRY_PATH") {
pub fn default_registry_path() -> PathBuf {
if let Some(path) = env::var_os("UPM_REGISTRY_PATH") {
return PathBuf::from(path);
}
let home = env::var_os("HOME").unwrap_or_else(|| ".".into());
PathBuf::from(home).join(".local/share/aim/registry.toml")
PathBuf::from(home).join(".local/share/upm/registry.toml")
}
fn registry_path() -> PathBuf {
default_registry_path()
}
#[derive(Debug, Eq, PartialEq)]
@ -213,49 +234,49 @@ pub enum DispatchResult {
#[derive(Debug)]
pub enum DispatchError {
AddPlan(aim_core::app::add::BuildAddPlanError),
AddInstall(aim_core::app::add::InstallAppError),
AddPlan(upm_core::app::add::BuildAddPlanError),
AddInstall(upm_core::app::add::InstallAppError),
Prompt(ui::prompt::PromptError),
RemovePlan(aim_core::app::remove::RemoveRegisteredAppError),
Registry(aim_core::registry::store::RegistryStoreError),
Search(aim_core::app::search::SearchError),
Show(aim_core::domain::show::ShowResultError),
UpdatePlan(aim_core::app::update::BuildUpdatePlanError),
UpdateExecution(aim_core::app::update::ExecuteUpdatesError),
RemovePlan(upm_core::app::remove::RemoveRegisteredAppError),
Registry(upm_core::registry::store::RegistryStoreError),
Search(upm_core::app::search::SearchError),
Show(upm_core::domain::show::ShowResultError),
UpdatePlan(upm_core::app::update::BuildUpdatePlanError),
UpdateExecution(upm_core::app::update::ExecuteUpdatesError),
}
impl std::fmt::Display for DispatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AddPlan(error) => match error {
aim_core::app::add::BuildAddPlanError::Query(
aim_core::app::query::ResolveQueryError::Unsupported,
upm_core::app::add::BuildAddPlanError::Query(
upm_core::app::query::ResolveQueryError::Unsupported,
) => write!(f, "unsupported source query"),
aim_core::app::add::BuildAddPlanError::InsecureHttpSource { .. } => write!(
upm_core::app::add::BuildAddPlanError::InsecureHttpSource { .. } => write!(
f,
"insecure HTTP sources are disabled; set allow_http = true to permit them"
),
aim_core::app::add::BuildAddPlanError::NoInstallableArtifact { source } => write!(
upm_core::app::add::BuildAddPlanError::NoInstallableArtifact { source } => write!(
f,
"no installable artifact found for {} {}",
source.kind.as_str(),
source.locator
),
aim_core::app::add::BuildAddPlanError::Adapter(id, error) => match error {
aim_core::adapters::traits::AdapterError::UnsupportedQuery => {
upm_core::app::add::BuildAddPlanError::Adapter(id, error) => match error {
upm_core::adapters::traits::AdapterError::UnsupportedQuery => {
write!(f, "{id} does not support this query")
}
aim_core::adapters::traits::AdapterError::UnsupportedSource => {
upm_core::adapters::traits::AdapterError::UnsupportedSource => {
write!(f, "{id} does not support this source")
}
aim_core::adapters::traits::AdapterError::ResolutionFailed(reason) => {
upm_core::adapters::traits::AdapterError::ResolutionFailed(reason) => {
write!(f, "{id} resolution failed: {reason}")
}
},
aim_core::app::add::BuildAddPlanError::GitHubDiscovery(error) => {
upm_core::app::add::BuildAddPlanError::GitHubDiscovery(error) => {
write!(f, "github discovery failed: {error:?}")
}
aim_core::app::add::BuildAddPlanError::NoCandidates => {
upm_core::app::add::BuildAddPlanError::NoCandidates => {
write!(f, "no installable candidates found")
}
},
@ -265,7 +286,7 @@ impl std::fmt::Display for DispatchError {
Self::Registry(error) => write!(f, "registry failed: {error:?}"),
Self::Search(error) => write!(f, "search failed: {error:?}"),
Self::Show(error) => match error {
aim_core::domain::show::ShowResultError::AmbiguousInstalledMatch {
upm_core::domain::show::ShowResultError::AmbiguousInstalledMatch {
query,
matches,
} => write!(
@ -273,14 +294,14 @@ impl std::fmt::Display for DispatchError {
"multiple installed apps match {query}: {}",
matches.join(", ")
),
aim_core::domain::show::ShowResultError::UnsupportedQuery => {
upm_core::domain::show::ShowResultError::UnsupportedQuery => {
write!(f, "unsupported source query")
}
aim_core::domain::show::ShowResultError::InsecureHttpSource => write!(
upm_core::domain::show::ShowResultError::InsecureHttpSource => write!(
f,
"insecure HTTP sources are disabled; set allow_http = true to permit them"
),
aim_core::domain::show::ShowResultError::NoInstallableArtifact { source } => {
upm_core::domain::show::ShowResultError::NoInstallableArtifact { source } => {
write!(
f,
"no installable artifact found for {} {}",
@ -288,18 +309,18 @@ impl std::fmt::Display for DispatchError {
source.locator
)
}
aim_core::domain::show::ShowResultError::AdapterResolutionFailed {
upm_core::domain::show::ShowResultError::AdapterResolutionFailed {
adapter_id,
kind,
detail,
} => match kind {
aim_core::domain::show::AdapterFailureKind::UnsupportedQuery => {
upm_core::domain::show::AdapterFailureKind::UnsupportedQuery => {
write!(f, "{adapter_id} does not support this query")
}
aim_core::domain::show::AdapterFailureKind::UnsupportedSource => {
upm_core::domain::show::AdapterFailureKind::UnsupportedSource => {
write!(f, "{adapter_id} does not support this source")
}
aim_core::domain::show::AdapterFailureKind::ResolutionFailed => {
upm_core::domain::show::AdapterFailureKind::ResolutionFailed => {
if let Some(detail) = detail {
write!(f, "{adapter_id} resolution failed: {detail}")
} else {
@ -307,27 +328,27 @@ impl std::fmt::Display for DispatchError {
}
}
},
aim_core::domain::show::ShowResultError::GitHubDiscoveryFailed {
upm_core::domain::show::ShowResultError::GitHubDiscoveryFailed {
kind,
detail,
} => match (kind, detail) {
(
aim_core::domain::show::GitHubDiscoveryFailureKind::FixtureDocumentMissing,
upm_core::domain::show::GitHubDiscoveryFailureKind::FixtureDocumentMissing,
Some(detail),
) => write!(f, "github discovery failed: missing fixture document {detail}"),
(
aim_core::domain::show::GitHubDiscoveryFailureKind::NoReleases,
upm_core::domain::show::GitHubDiscoveryFailureKind::NoReleases,
Some(detail),
) => write!(f, "github discovery failed: no releases for {detail}"),
(aim_core::domain::show::GitHubDiscoveryFailureKind::Unsupported, _) => {
(upm_core::domain::show::GitHubDiscoveryFailureKind::Unsupported, _) => {
write!(f, "github discovery failed: unsupported source")
}
(aim_core::domain::show::GitHubDiscoveryFailureKind::Transport, _) => {
(upm_core::domain::show::GitHubDiscoveryFailureKind::Transport, _) => {
write!(f, "github discovery failed: transport error")
}
_ => write!(f, "github discovery failed"),
},
aim_core::domain::show::ShowResultError::NoInstallableCandidates => {
upm_core::domain::show::ShowResultError::NoInstallableCandidates => {
write!(f, "no installable candidates found")
}
},
@ -337,25 +358,25 @@ impl std::fmt::Display for DispatchError {
}
}
fn render_install_error(error: &aim_core::app::add::InstallAppError) -> String {
fn render_install_error(error: &upm_core::app::add::InstallAppError) -> String {
match error {
aim_core::app::add::InstallAppError::Materialize(error) => format!("{error:?}"),
aim_core::app::add::InstallAppError::Policy(error) => error.clone(),
aim_core::app::add::InstallAppError::Download(error) => error.to_string(),
aim_core::app::add::InstallAppError::DownloadIo(error) => error.to_string(),
aim_core::app::add::InstallAppError::HostProbe(error) => error.to_string(),
aim_core::app::add::InstallAppError::Install(error) => error.to_string(),
upm_core::app::add::InstallAppError::Materialize(error) => format!("{error:?}"),
upm_core::app::add::InstallAppError::Policy(error) => error.clone(),
upm_core::app::add::InstallAppError::Download(error) => error.to_string(),
upm_core::app::add::InstallAppError::DownloadIo(error) => error.to_string(),
upm_core::app::add::InstallAppError::HostProbe(error) => error.to_string(),
upm_core::app::add::InstallAppError::Install(error) => error.to_string(),
}
}
impl From<aim_core::app::add::BuildAddPlanError> for DispatchError {
fn from(value: aim_core::app::add::BuildAddPlanError) -> Self {
impl From<upm_core::app::add::BuildAddPlanError> for DispatchError {
fn from(value: upm_core::app::add::BuildAddPlanError) -> Self {
Self::AddPlan(value)
}
}
impl From<aim_core::app::add::InstallAppError> for DispatchError {
fn from(value: aim_core::app::add::InstallAppError) -> Self {
impl From<upm_core::app::add::InstallAppError> for DispatchError {
fn from(value: upm_core::app::add::InstallAppError) -> Self {
Self::AddInstall(value)
}
}
@ -366,38 +387,38 @@ impl From<ui::prompt::PromptError> for DispatchError {
}
}
impl From<aim_core::app::update::BuildUpdatePlanError> for DispatchError {
fn from(value: aim_core::app::update::BuildUpdatePlanError) -> Self {
impl From<upm_core::app::update::BuildUpdatePlanError> for DispatchError {
fn from(value: upm_core::app::update::BuildUpdatePlanError) -> Self {
Self::UpdatePlan(value)
}
}
impl From<aim_core::app::update::ExecuteUpdatesError> for DispatchError {
fn from(value: aim_core::app::update::ExecuteUpdatesError) -> Self {
impl From<upm_core::app::update::ExecuteUpdatesError> for DispatchError {
fn from(value: upm_core::app::update::ExecuteUpdatesError) -> Self {
Self::UpdateExecution(value)
}
}
impl From<aim_core::app::remove::RemoveRegisteredAppError> for DispatchError {
fn from(value: aim_core::app::remove::RemoveRegisteredAppError) -> Self {
impl From<upm_core::app::remove::RemoveRegisteredAppError> for DispatchError {
fn from(value: upm_core::app::remove::RemoveRegisteredAppError) -> Self {
Self::RemovePlan(value)
}
}
impl From<aim_core::registry::store::RegistryStoreError> for DispatchError {
fn from(value: aim_core::registry::store::RegistryStoreError) -> Self {
impl From<upm_core::registry::store::RegistryStoreError> for DispatchError {
fn from(value: upm_core::registry::store::RegistryStoreError) -> Self {
Self::Registry(value)
}
}
impl From<aim_core::app::search::SearchError> for DispatchError {
fn from(value: aim_core::app::search::SearchError) -> Self {
impl From<upm_core::app::search::SearchError> for DispatchError {
fn from(value: upm_core::app::search::SearchError) -> Self {
Self::Search(value)
}
}
impl From<aim_core::domain::show::ShowResultError> for DispatchError {
fn from(value: aim_core::domain::show::ShowResultError) -> Self {
impl From<upm_core::domain::show::ShowResultError> for DispatchError {
fn from(value: upm_core::domain::show::ShowResultError) -> Self {
Self::Show(value)
}
}
@ -442,7 +463,7 @@ fn merge_updated_app_records(
}
fn install_home(registry_path: &Path) -> PathBuf {
if env::var_os("AIM_REGISTRY_PATH").is_some() {
if env::var_os("UPM_REGISTRY_PATH").is_some() {
return registry_path
.parent()
.unwrap_or_else(|| Path::new("."))
@ -454,7 +475,7 @@ fn install_home(registry_path: &Path) -> PathBuf {
}
fn is_effective_root() -> bool {
if let Some(value) = env::var_os("AIM_EFFECTIVE_ROOT") {
if let Some(value) = env::var_os("UPM_EFFECTIVE_ROOT") {
let value = value.to_string_lossy();
return value == "1" || value.eq_ignore_ascii_case("true");
}

View file

@ -1,16 +1,16 @@
fn main() {
let loaded_theme_config = aim_cli::cli::config::AppConfig::load();
aim_cli::ui::theme::set_active_theme(aim_cli::ui::theme::resolve_theme(
let loaded_theme_config = upm::cli::config::AppConfig::load();
upm::ui::theme::set_active_theme(upm::ui::theme::resolve_theme(
&loaded_theme_config.config.theme,
));
for warning in loaded_theme_config.warnings {
eprintln!(
"{}",
aim_cli::ui::theme::warning_text(&format!("Config warning: {warning}"))
upm::ui::theme::warning_text(&format!("Config warning: {warning}"))
);
}
let config = match aim_cli::config::load() {
let config = match upm::config::load() {
Ok(config) => config,
Err(error) => {
eprintln!("{error}");
@ -18,11 +18,11 @@ fn main() {
}
};
let cli = aim_cli::parse();
let mut reporter = aim_cli::ui::progress::TerminalProgressReporter::stderr();
match aim_cli::dispatch_with_reporter_and_config(cli, &config, &mut reporter) {
let cli = upm::parse();
let mut reporter = upm::ui::progress::TerminalProgressReporter::stderr();
match upm::dispatch_with_reporter_and_config(cli, &config, &mut reporter) {
Ok(result) => {
let output = aim_cli::render_with_config(&result, &config);
let output = upm::render_with_config(&result, &config);
if !output.is_empty() {
if reporter.emitted_output() {
println!();

View file

@ -0,0 +1,16 @@
use upm_appimage::AppImageHubAddProvider;
use upm_appimage::AppImageHubSearchProvider;
use upm_appimage::source::appimagehub;
use upm_core::ProviderRegistry;
pub fn with_provider_registry<T>(build: impl FnOnce(&ProviderRegistry<'_>) -> T) -> T {
let appimagehub_transport = appimagehub::default_transport();
let appimagehub_search = AppImageHubSearchProvider::new(appimagehub_transport.as_ref());
let appimagehub_add = AppImageHubAddProvider::new(appimagehub_transport.as_ref());
let providers = ProviderRegistry {
search_providers: vec![&appimagehub_search],
external_add_providers: vec![&appimagehub_add],
};
build(&providers)
}

View file

@ -1,8 +1,8 @@
use std::io::IsTerminal;
use std::time::Duration;
use aim_core::app::progress::{OperationEvent, OperationKind, OperationStage, ProgressReporter};
use indicatif::{ProgressBar, ProgressStyle};
use upm_core::app::progress::{OperationEvent, OperationKind, OperationStage, ProgressReporter};
pub fn new_progress_bar(total: Option<u64>) -> ProgressBar {
match total {
@ -241,7 +241,7 @@ impl ProgressReporter for TerminalProgressReporter {
mod tests {
use super::TerminalProgressReporter;
use crate::ui::progress::{ProgressReporter, format_completed_stage_line};
use aim_core::app::progress::{OperationEvent, OperationStage};
use upm_core::app::progress::{OperationEvent, OperationStage};
#[test]
fn stage_change_resets_byte_progress_position() {

View file

@ -1,11 +1,11 @@
use std::env;
use std::io::IsTerminal;
use aim_core::app::add::{AddPlan, prefer_latest_tracking};
use aim_core::app::interaction::{InteractionKind, InteractionRequest};
use dialoguer::Select;
use upm_core::app::add::{AddPlan, prefer_latest_tracking};
use upm_core::app::interaction::{InteractionKind, InteractionRequest};
const TRACKING_PREFERENCE_ENV: &str = "AIM_TRACKING_PREFERENCE";
const TRACKING_PREFERENCE_ENV: &str = "UPM_TRACKING_PREFERENCE";
pub fn render_interaction(request: &InteractionRequest) -> String {
match &request.kind {

Some files were not shown because too many files have changed in this diff Show more