From 863c57e473ce041c5f4c9f9a462d6d037e8b233a Mon Sep 17 00:00:00 2001 From: stoorps Date: Sat, 21 Mar 2026 22:39:11 +0000 Subject: [PATCH] refactor: rename aim to upm and extract appimage module --- .architecture/overview.md | 31 +- .architecture/roadmap.md | 208 +++++++ ...and-core-extraction-implementation-plan.md | 410 ++++++++++++++ AGENTS.md | 6 +- Cargo.lock | 81 +-- Cargo.toml | 7 +- README.md | 50 +- crates/aim-cli/tests/config_loading.rs | 66 --- crates/aim-core/tests/install_paths.rs | 21 - crates/upm-appimage/Cargo.toml | 14 + crates/upm-appimage/src/add.rs | 163 ++++++ crates/upm-appimage/src/lib.rs | 6 + crates/upm-appimage/src/search.rs | 103 ++++ crates/upm-appimage/src/source/appimagehub.rs | 517 ++++++++++++++++++ crates/upm-appimage/src/source/mod.rs | 1 + .../tests/appimagehub_search.rs | 57 +- crates/{aim-core => upm-core}/Cargo.toml | 2 +- .../src/adapters/appimagehub.rs | 0 .../src/adapters/direct_url.rs | 0 .../src/adapters/github.rs | 0 .../src/adapters/gitlab.rs | 0 .../src/adapters/mod.rs | 10 +- .../src/adapters/sourceforge.rs | 0 .../src/adapters/test_support.rs | 0 .../src/adapters/traits.rs | 0 .../src/adapters/zsync.rs | 0 crates/{aim-core => upm-core}/src/app/add.rs | 154 ++++-- .../src/app/identity.rs | 0 .../src/app/interaction.rs | 0 crates/{aim-core => upm-core}/src/app/list.rs | 0 crates/{aim-core => upm-core}/src/app/mod.rs | 1 + .../src/app/progress.rs | 0 crates/upm-core/src/app/providers.rs | 24 + .../{aim-core => upm-core}/src/app/query.rs | 0 .../{aim-core => upm-core}/src/app/remove.rs | 0 .../{aim-core => upm-core}/src/app/scope.rs | 0 .../{aim-core => upm-core}/src/app/search.rs | 119 +--- crates/{aim-core => upm-core}/src/app/show.rs | 0 .../{aim-core => upm-core}/src/app/update.rs | 2 +- .../{aim-core => upm-core}/src/domain/app.rs | 0 .../{aim-core => upm-core}/src/domain/mod.rs | 0 .../src/domain/search.rs | 0 .../{aim-core => upm-core}/src/domain/show.rs | 0 .../src/domain/source.rs | 0 .../src/domain/update.rs | 0 .../src/integration/desktop.rs | 0 .../src/integration/install.rs | 0 .../src/integration/mod.rs | 0 .../src/integration/paths.rs | 2 +- .../src/integration/policy.rs | 4 +- .../src/integration/refresh.rs | 14 +- crates/{aim-core => upm-core}/src/lib.rs | 2 + .../src/metadata/document.rs | 0 .../src/metadata/electron_builder.rs | 0 .../src/metadata/mod.rs | 0 .../src/metadata/parser.rs | 0 .../src/metadata/zsync.rs | 0 .../src/platform/capabilities.rs | 2 +- .../src/platform/distro.rs | 0 .../src/platform/mod.rs | 8 +- .../src/registry/mod.rs | 0 .../src/registry/model.rs | 0 .../src/registry/store.rs | 0 .../src/source/appimagehub.rs | 14 +- .../src/source/github.rs | 13 +- .../src/source/input.rs | 0 .../{aim-core => upm-core}/src/source/mod.rs | 1 - .../src/update/channels.rs | 0 .../{aim-core => upm-core}/src/update/mod.rs | 0 .../src/update/ranking.rs | 0 .../tests/adapter_contract.rs | 70 +-- .../tests/adapter_smoke.rs | 4 +- .../tests/checksum_verification.rs | 4 +- .../tests/download_pipeline.rs | 12 +- .../tests/fixtures/example.zsync | 0 .../tests/fixtures/latest-linux.yml | 0 .../tests/github_add_flow.rs | 6 +- .../tests/github_source_discovery.rs | 6 +- .../tests/identity_resolution.rs | 6 +- .../tests/install_failures.rs | 24 +- .../tests/install_integration.rs | 34 +- crates/upm-core/tests/install_paths.rs | 28 + .../tests/install_payload.rs | 2 +- .../tests/install_policy.rs | 6 +- .../tests/install_scope.rs | 4 +- .../tests/metadata_contract.rs | 4 +- .../tests/metadata_electron_builder.rs | 4 +- .../tests/metadata_zsync.rs | 4 +- .../tests/platform_detection.rs | 4 +- crates/upm-core/tests/provider_registry.rs | 159 ++++++ .../tests/query_resolution.rs | 26 +- .../tests/registry_roundtrip.rs | 82 +-- .../tests/remove_flow.rs | 32 +- .../tests/search_github.rs | 12 +- .../tests/show_resolution.rs | 18 +- .../tests/update_planning.rs | 38 +- crates/{aim-cli => upm}/Cargo.toml | 7 +- crates/{aim-cli => upm}/src/cli/args.rs | 4 +- crates/{aim-cli => upm}/src/cli/config.rs | 4 +- crates/{aim-cli => upm}/src/cli/mod.rs | 0 crates/{aim-cli => upm}/src/config.rs | 6 +- crates/{aim-cli => upm}/src/lib.rs | 197 ++++--- crates/{aim-cli => upm}/src/main.rs | 16 +- crates/upm/src/providers.rs | 16 + crates/{aim-cli => upm}/src/ui/mod.rs | 0 crates/{aim-cli => upm}/src/ui/progress.rs | 4 +- crates/{aim-cli => upm}/src/ui/prompt.rs | 6 +- crates/{aim-cli => upm}/src/ui/render.rs | 40 +- .../{aim-cli => upm}/src/ui/search_browser.rs | 2 +- crates/{aim-cli => upm}/src/ui/theme.rs | 0 crates/{aim-cli => upm}/tests/cli_commands.rs | 18 +- crates/{aim-cli => upm}/tests/cli_smoke.rs | 2 +- crates/upm/tests/config_loading.rs | 167 ++++++ .../{aim-cli => upm}/tests/end_to_end_cli.rs | 180 +++--- .../{aim-cli => upm}/tests/search_browser.rs | 12 +- crates/{aim-cli => upm}/tests/search_cli.rs | 38 +- crates/{aim-cli => upm}/tests/ui_summary.rs | 88 +-- 117 files changed, 2622 insertions(+), 887 deletions(-) create mode 100644 .architecture/roadmap.md create mode 100644 .plans/013-upm-rename-and-core-extraction/2026-03-21-upm-rename-and-core-extraction-implementation-plan.md delete mode 100644 crates/aim-cli/tests/config_loading.rs delete mode 100644 crates/aim-core/tests/install_paths.rs create mode 100644 crates/upm-appimage/Cargo.toml create mode 100644 crates/upm-appimage/src/add.rs create mode 100644 crates/upm-appimage/src/lib.rs create mode 100644 crates/upm-appimage/src/search.rs create mode 100644 crates/upm-appimage/src/source/appimagehub.rs create mode 100644 crates/upm-appimage/src/source/mod.rs rename crates/{aim-core => upm-appimage}/tests/appimagehub_search.rs (63%) rename crates/{aim-core => upm-core}/Cargo.toml (95%) rename crates/{aim-core => upm-core}/src/adapters/appimagehub.rs (100%) rename crates/{aim-core => upm-core}/src/adapters/direct_url.rs (100%) rename crates/{aim-core => upm-core}/src/adapters/github.rs (100%) rename crates/{aim-core => upm-core}/src/adapters/gitlab.rs (100%) rename crates/{aim-core => upm-core}/src/adapters/mod.rs (75%) rename crates/{aim-core => upm-core}/src/adapters/sourceforge.rs (100%) rename crates/{aim-core => upm-core}/src/adapters/test_support.rs (100%) rename crates/{aim-core => upm-core}/src/adapters/traits.rs (100%) rename crates/{aim-core => upm-core}/src/adapters/zsync.rs (100%) rename crates/{aim-core => upm-core}/src/app/add.rs (87%) rename crates/{aim-core => upm-core}/src/app/identity.rs (100%) rename crates/{aim-core => upm-core}/src/app/interaction.rs (100%) rename crates/{aim-core => upm-core}/src/app/list.rs (100%) rename crates/{aim-core => upm-core}/src/app/mod.rs (90%) rename crates/{aim-core => upm-core}/src/app/progress.rs (100%) create mode 100644 crates/upm-core/src/app/providers.rs rename crates/{aim-core => upm-core}/src/app/query.rs (100%) rename crates/{aim-core => upm-core}/src/app/remove.rs (100%) rename crates/{aim-core => upm-core}/src/app/scope.rs (100%) rename crates/{aim-core => upm-core}/src/app/search.rs (77%) rename crates/{aim-core => upm-core}/src/app/show.rs (100%) rename crates/{aim-core => upm-core}/src/app/update.rs (99%) rename crates/{aim-core => upm-core}/src/domain/app.rs (100%) rename crates/{aim-core => upm-core}/src/domain/mod.rs (100%) rename crates/{aim-core => upm-core}/src/domain/search.rs (100%) rename crates/{aim-core => upm-core}/src/domain/show.rs (100%) rename crates/{aim-core => upm-core}/src/domain/source.rs (100%) rename crates/{aim-core => upm-core}/src/domain/update.rs (100%) rename crates/{aim-core => upm-core}/src/integration/desktop.rs (100%) rename crates/{aim-core => upm-core}/src/integration/install.rs (100%) rename crates/{aim-core => upm-core}/src/integration/mod.rs (100%) rename crates/{aim-core => upm-core}/src/integration/paths.rs (95%) rename crates/{aim-core => upm-core}/src/integration/policy.rs (94%) rename crates/{aim-core => upm-core}/src/integration/refresh.rs (86%) rename crates/{aim-core => upm-core}/src/lib.rs (63%) rename crates/{aim-core => upm-core}/src/metadata/document.rs (100%) rename crates/{aim-core => upm-core}/src/metadata/electron_builder.rs (100%) rename crates/{aim-core => upm-core}/src/metadata/mod.rs (100%) rename crates/{aim-core => upm-core}/src/metadata/parser.rs (100%) rename crates/{aim-core => upm-core}/src/metadata/zsync.rs (100%) rename crates/{aim-core => upm-core}/src/platform/capabilities.rs (97%) rename crates/{aim-core => upm-core}/src/platform/distro.rs (100%) rename crates/{aim-core => upm-core}/src/platform/mod.rs (93%) rename crates/{aim-core => upm-core}/src/registry/mod.rs (100%) rename crates/{aim-core => upm-core}/src/registry/model.rs (100%) rename crates/{aim-core => upm-core}/src/registry/store.rs (100%) rename crates/{aim-core => upm-core}/src/source/appimagehub.rs (97%) rename crates/{aim-core => upm-core}/src/source/github.rs (97%) rename crates/{aim-core => upm-core}/src/source/input.rs (100%) rename crates/{aim-core => upm-core}/src/source/mod.rs (59%) rename crates/{aim-core => upm-core}/src/update/channels.rs (100%) rename crates/{aim-core => upm-core}/src/update/mod.rs (100%) rename crates/{aim-core => upm-core}/src/update/ranking.rs (100%) rename crates/{aim-core => upm-core}/tests/adapter_contract.rs (86%) rename crates/{aim-core => upm-core}/tests/adapter_smoke.rs (79%) rename crates/{aim-core => upm-core}/tests/checksum_verification.rs (97%) rename crates/{aim-core => upm-core}/tests/download_pipeline.rs (95%) rename crates/{aim-core => upm-core}/tests/fixtures/example.zsync (100%) rename crates/{aim-core => upm-core}/tests/fixtures/latest-linux.yml (100%) rename crates/{aim-core => upm-core}/tests/github_add_flow.rs (94%) rename crates/{aim-core => upm-core}/tests/github_source_discovery.rs (94%) rename crates/{aim-core => upm-core}/tests/identity_resolution.rs (86%) rename crates/{aim-core => upm-core}/tests/install_failures.rs (87%) rename crates/{aim-core => upm-core}/tests/install_integration.rs (95%) create mode 100644 crates/upm-core/tests/install_paths.rs rename crates/{aim-core => upm-core}/tests/install_payload.rs (94%) rename crates/{aim-core => upm-core}/tests/install_policy.rs (87%) rename crates/{aim-core => upm-core}/tests/install_scope.rs (63%) rename crates/{aim-core => upm-core}/tests/metadata_contract.rs (73%) rename crates/{aim-core => upm-core}/tests/metadata_electron_builder.rs (80%) rename crates/{aim-core => upm-core}/tests/metadata_zsync.rs (76%) rename crates/{aim-core => upm-core}/tests/platform_detection.rs (93%) create mode 100644 crates/upm-core/tests/provider_registry.rs rename crates/{aim-core => upm-core}/tests/query_resolution.rs (94%) rename crates/{aim-core => upm-core}/tests/registry_roundtrip.rs (78%) rename crates/{aim-core => upm-core}/tests/remove_flow.rs (86%) rename crates/{aim-core => upm-core}/tests/search_github.rs (95%) rename crates/{aim-core => upm-core}/tests/show_resolution.rs (95%) rename crates/{aim-core => upm-core}/tests/update_planning.rs (95%) rename crates/{aim-cli => upm}/Cargo.toml (81%) rename crates/{aim-cli => upm}/src/cli/args.rs (89%) rename crates/{aim-cli => upm}/src/cli/config.rs (97%) rename crates/{aim-cli => upm}/src/cli/mod.rs (100%) rename crates/{aim-cli => upm}/src/config.rs (94%) rename crates/{aim-cli => upm}/src/lib.rs (69%) rename crates/{aim-cli => upm}/src/main.rs (53%) create mode 100644 crates/upm/src/providers.rs rename crates/{aim-cli => upm}/src/ui/mod.rs (100%) rename crates/{aim-cli => upm}/src/ui/progress.rs (98%) rename crates/{aim-cli => upm}/src/ui/prompt.rs (94%) rename crates/{aim-cli => upm}/src/ui/render.rs (94%) rename crates/{aim-cli => upm}/src/ui/search_browser.rs (99%) rename crates/{aim-cli => upm}/src/ui/theme.rs (100%) rename crates/{aim-cli => upm}/tests/cli_commands.rs (78%) rename crates/{aim-cli => upm}/tests/cli_smoke.rs (64%) create mode 100644 crates/upm/tests/config_loading.rs rename crates/{aim-cli => upm}/tests/end_to_end_cli.rs (82%) rename crates/{aim-cli => upm}/tests/search_browser.rs (93%) rename crates/{aim-cli => upm}/tests/search_cli.rs (82%) rename crates/{aim-cli => upm}/tests/ui_summary.rs (88%) diff --git a/.architecture/overview.md b/.architecture/overview.md index 2e648d9..478b8ae 100644 --- a/.architecture/overview.md +++ b/.architecture/overview.md @@ -2,20 +2,22 @@ ## Workspace Shape -`aim` is a Rust workspace with two main crates: +`upm` is a Rust workspace with three main crates: -- `crates/aim-core`: source normalization, provider adapters, install/update planning, payload installation, registry persistence, and desktop integration. -- `crates/aim-cli`: argument parsing, config loading, terminal UX, prompting, progress reporting, and summary rendering. +- `crates/upm-core`: source normalization, add/update orchestration, registry persistence, install policies, desktop integration, and the provider-composition seam. +- `crates/upm`: argument parsing, config loading, terminal UX, prompting, progress reporting, summary rendering, and provider assembly. +- `crates/upm-appimage`: AppImageHub transport, search-provider behavior, and exact add-resolution for AppImage-backed installs. -The split keeps product logic in `aim-core` so additional frontends can reuse the same install and update pipeline. +The split keeps frontend-agnostic logic in `upm-core`, while concrete package-source behavior is composed at the CLI boundary. That keeps the headless layer reusable for future frontends without making provider behavior a permanent core dependency. ## Core Flow The main execution path is: -1. Parse CLI input and load runtime config in `aim-cli`. -2. Resolve the query into a normalized source in `aim-core`. -3. Build an add or update plan through provider adapters and artifact selection. +1. Parse CLI input and load runtime config in `upm`. +2. Assemble a `ProviderRegistry` in `crates/upm/src/providers.rs`. +3. Resolve the query into a normalized source in `upm-core`. +4. Build an add or update plan through core orchestration plus any registered external providers. 4. Download the selected AppImage into a staged path. 5. Verify integrity metadata when available. 6. Commit the payload into the managed install location. @@ -33,7 +35,16 @@ Supported source classes currently include: - direct URLs - local file imports -Provider-specific resolution lives in `crates/aim-core/src/adapters` and `crates/aim-core/src/source`. +Core source normalization and orchestration live in `crates/upm-core`. AppImageHub-specific transport and provider behavior live in `crates/upm-appimage` and are injected through `ProviderRegistry` rather than hardcoded into core entrypoints. + +## Runtime Interface + +The rename to `upm` is a hard cutover: + +- runtime overrides use `UPM_*` +- legacy `AIM_*` runtime overrides are not read +- default config, registry, payload, and desktop-entry paths use `upm` names +- helper audit logging uses `UPM_DEBUG_EXTERNAL_HELPERS=1` ## Security Hardening State @@ -51,9 +62,9 @@ The remaining deferred AppImageHub host-trust concern is tracked in `security-is ## Persistence And Integration -- Registry writes are atomic and live under the registry store implementation in `aim-core`. +- Registry writes are atomic and live under the registry store implementation in `upm-core`. - Managed payload, desktop entry, and icon paths are resolved from install policy and scope. -- Desktop integration refresh uses external helpers when available and now supports env-gated audit logging through `AIM_DEBUG_EXTERNAL_HELPERS=1`. +- Desktop integration refresh uses external helpers when available and now supports env-gated audit logging through `UPM_DEBUG_EXTERNAL_HELPERS=1`. ## Planning And Audit Artifacts diff --git a/.architecture/roadmap.md b/.architecture/roadmap.md new file mode 100644 index 0000000..4c76526 --- /dev/null +++ b/.architecture/roadmap.md @@ -0,0 +1,208 @@ +# UPM Roadmap + +## Direction + +The project is evolving from a focused AppImage manager into `upm`, a modular universal package manager. The target system manages multiple package sources through a shared headless core, keeps the CLI thin, and leaves room for future GUI frontends. + +The initial rename-and-extraction slice has now landed as a hard cutover: the runtime surface is `upm`, the shared core is `upm-core`, and AppImage support is composed through `upm-appimage` instead of being embedded directly into the core. + +The near-term goal is a Linux-first platform with honest cross-platform architecture. Phase 2 will implement Linux package sources while establishing the abstractions needed for later macOS support and possible Windows support. + +## Confirmed Product Decisions + +- `aim-cli` becomes `upm`. +- `aim-core` stops being the all-in-one backend and is split. +- AppImage support becomes an installable module named `upm-appimage`. +- Shared orchestration, config, state, resolution, ranking, and frontend-facing APIs move into `upm-core`. +- The `upm` crate becomes a thin CLI client over `upm-core`. +- The rename is a hard cutover. Legacy `AIM_*` runtime interfaces are removed rather than preserved. +- The declarative package file starts in a hybrid mode and is intended to become the source of truth over time. +- Every `upm` invocation should detect drift between declared state and observed system state, then auto-sync metadata as needed. +- Phase 2 is Linux implementation first, with macOS-oriented provider abstractions and packaging seams designed now. +- Feature delivery should happen as vertical slices rather than a single large refactor. +- The `upm` branch is the effective trunk for this evolution work and should be treated as the integration base for future UPM feature branches and worktrees. + +## Architectural Destination + +### Workspace Shape + +The intended workspace shape after the initial refactor is: + +- `upm`: thin CLI frontend, ratatui config UI, command routing, presentation. +- `upm-core`: headless application layer, provider registry, resolution pipeline, state model, declarative sync engine, ranking, policies, and frontend-agnostic APIs. +- `upm-appimage`: AppImage provider module extracted from the current `aim-core` implementation. +- future provider modules: `upm-pacman`, `upm-aur`, `upm-flatpak`, `upm-cargo`, `upm-npm`, and later macOS or Windows-specific modules. + +### Module Model + +UPM should stay modular in both code and packaging: + +- modules can be enabled or disabled by config +- providers can be ranked by user priority +- distro packaging can offer grouped installs such as `upm-full` +- lighter installs can ship only the core and selected modules + +This means provider capabilities, discovery, search, install, remove, inspect, and sync behavior need stable interfaces in `upm-core` rather than provider-specific branching in the CLI. + +### State Model + +The long-term model is declarative and config-first, but Phase 2 begins with a hybrid approach: + +- `upm`-managed actions update the declarative config directly +- `upm update` and normal command entrypoints can inspect live system state +- drift detection reconciles unmanaged or changed packages into the config representation +- over time the config becomes authoritative and reconciliation becomes stricter + +## Phase 2 Milestones + +### Milestone 0: Rename And Split Foundation + +Deliver the naming and ownership transition without changing product scope yet. + +Goals: + +- rename workspace crates and package outputs from `aim` to `upm` +- create `upm-core` by extracting reusable infrastructure from `aim-core` +- reduce the CLI crate to a frontend over headless APIs +- isolate current AppImage-specific logic into `upm-appimage` +- compose provider behavior in the CLI through `ProviderRegistry` rather than hardcoded AppImage paths in `upm-core` +- preserve current AppImage functionality and tests during the move + +Exit criteria: + +- `upm` binary replaces `aim` +- workspace builds under new crate names +- AppImage flows still work end-to-end through the new layering + +### Milestone 1: AppImage On The New Core + +Make the current AppImage implementation the first real module on the modular architecture. + +Goals: + +- validate the provider module contract using AppImage as the reference implementation +- move search, add, install, update, show, and remove behaviors behind core provider APIs +- prove the CLI can treat AppImage as just one enabled source + +Exit criteria: + +- AppImage support is no longer special-cased as the whole product +- provider registration and capability discovery exist in `upm-core` + +### Milestone 2: Linux Native Sources + +Add the first non-AppImage providers as Linux-focused vertical slices. + +Initial supported sources: + +- pacman +- AUR +- Flatpak +- cargo global installs +- npm global installs + +Goals: + +- implement provider discovery and capability coverage for each source +- normalize package identity, version, installed state, and update candidates across providers +- support search, inspect, install, remove, and update planning where each provider can do so safely +- capture provider limitations explicitly rather than faking uniformity + +Exit criteria: + +- multi-source search works across enabled providers +- installs and removals work through a consistent command model +- provider-specific metadata is normalized enough for ranking and sync + +### Milestone 3: Provider Priority And Config UX + +Expose modularity directly in the terminal interface. + +Goals: + +- build a ratatui configuration menu +- allow enablement and disablement per provider +- allow explicit search and install priority ordering +- allow configuration of module weight, source preference, and future policy toggles +- make search ranking obey configured priority instead of hard-coded source bias + +Exit criteria: + +- users can manage provider selection and ranking from the TUI +- search results are explainable in terms of configured preference order + +### Milestone 4: Declarative Package State And Drift Sync + +Introduce the nix-like experience incrementally. + +Goals: + +- define the declarative package file format in `config.toml` or a closely related managed file +- track installed packages by provider in a normalized state model +- implement `upm update` as both package refresh and state reconciliation +- scan currently installed packages from supported providers and build or refresh the declared package set +- auto-sync detected drift during any `upm` command invocation + +Phase 2 intent: + +- begin in hybrid mode +- move steadily toward config-first behavior +- avoid destructive reconciliation until provider semantics are trustworthy + +Exit criteria: + +- users can bootstrap a declarative package definition from the current machine state +- repeated `upm` runs keep declared and observed state aligned +- state drift is surfaced clearly and reconciled predictably + +### Milestone 5: Packaging, Distribution, And Platform Seams + +Make the modular architecture real at packaging time, not just in code. + +Goals: + +- define packaging layout for standalone core, selected modules, and full installs +- support distro-level grouped packages such as `upm-full` +- ensure unsupported modules degrade cleanly on the wrong OS or distro +- add macOS provider and packaging seams to `upm-core` even if Linux remains the only implemented provider set in Phase 2 + +Exit criteria: + +- module packaging strategy is documented and testable +- cross-platform abstractions exist without blocking Linux delivery + +## Phase 2 Non-Goals + +The following are explicitly not required to complete Phase 2: + +- full macOS provider implementation +- Windows provider implementation +- GUI frontend delivery +- forcing strict config-authoritative reconciliation before provider behavior is stable +- shipping every conceivable Linux package manager in the first expansion + +## Success Criteria + +Phase 2 is successful when the project can credibly be described as a modular package manager rather than an AppImage manager with extra adapters. + +That means: + +- the product name, workspace shape, and binary identity are all `upm` +- AppImage support is only one module among several +- Linux users can manage packages from the first targeted provider set +- ranking and enablement are user-controlled through config and TUI +- declarative state exists, is importable from the live system, and stays synchronized through normal use +- the architecture is ready for GUI and later platform expansion without another major rewrite + +## Immediate Planning Order + +Implementation plans should follow this order: + +1. rename and crate extraction +2. provider API definition and AppImage migration onto `upm-core` +3. Linux provider onboarding in a stable order, likely `pacman` then `Flatpak`, then `AUR`, then `cargo`, then `npm` +4. ratatui configuration and ranking UX +5. declarative state model, drift detection, and `update` sync behavior +6. packaging layout and `upm-full` distribution strategy + +This order keeps the refactor defensible, gives each slice a usable product outcome, and avoids locking future provider work into AppImage-era assumptions. \ No newline at end of file diff --git a/.plans/013-upm-rename-and-core-extraction/2026-03-21-upm-rename-and-core-extraction-implementation-plan.md b/.plans/013-upm-rename-and-core-extraction/2026-03-21-upm-rename-and-core-extraction-implementation-plan.md new file mode 100644 index 0000000..acca473 --- /dev/null +++ b/.plans/013-upm-rename-and-core-extraction/2026-03-21-upm-rename-and-core-extraction-implementation-plan.md @@ -0,0 +1,410 @@ +# UPM Rename And Core Extraction Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Rename the product from `aim` to `upm`, remove legacy `aim` runtime interfaces, extract the shared headless backend into `upm-core`, and move AppImage-specific transport and provider logic into a separate `upm-appimage` module without regressing current AppImage workflows. + +**Architecture:** Execute this in vertical slices. First rename the workspace, binary, paths, environment interfaces, and tests to `upm` without carrying legacy `aim` compatibility. Next introduce a narrow provider-composition seam in `upm-core` so AppImage-specific add and search logic can move into `upm-appimage` without creating a dependency cycle. Finally rewire the `upm` CLI to assemble built-in providers, update docs, and run full verification. + +**Tech Stack:** Rust workspace, Cargo manifests, clap CLI, ratatui frontend crate, core domain/app modules, fixture-backed provider tests, workspace-wide `cargo test` and `cargo clippy`. + +--- + +### Task 1: Rename the workspace, binary, and default runtime paths to `upm` + +**Files:** +- Modify: `Cargo.toml` +- Rename: `crates/aim-cli` -> `crates/upm` +- Rename: `crates/aim-core` -> `crates/upm-core` +- Modify: `crates/upm/Cargo.toml` +- Modify: `crates/upm/src/main.rs` +- Modify: `crates/upm/src/lib.rs` +- Modify: `crates/upm/src/cli/args.rs` +- Modify: `crates/upm/src/config.rs` +- Modify: `crates/upm/src/cli/config.rs` +- Modify: `crates/upm-core/Cargo.toml` +- Modify: `crates/upm-core/src/platform/mod.rs` +- Modify: `crates/upm-core/src/integration/paths.rs` +- Modify: `crates/upm-core/src/integration/policy.rs` +- Test: `crates/upm/tests/cli_smoke.rs` +- Test: `crates/upm/tests/cli_commands.rs` +- Test: `crates/upm/tests/config_loading.rs` +- Test: `crates/upm-core/tests/install_paths.rs` +- Test: `crates/upm-core/tests/install_policy.rs` + +**Step 1: Write the failing rename expectations** + +Update the selected tests to assert: + +- the binary name is `upm` +- clap parses `upm` instead of `aim` +- default config path is `~/.config/upm/config.toml` +- default registry path is `~/.local/share/upm/registry.toml` +- default managed payload roots are `.local/lib/upm/appimages` and `/opt/upm/appimages` +- desktop entry filenames use `upm-.desktop` + +**Step 2: Run the focused tests to verify failure** + +Run: + +```bash +cargo test --package aim-cli --test cli_smoke +cargo test --package aim-cli --test config_loading +cargo test --package aim-core --test install_paths +cargo test --package aim-core --test install_policy +``` + +Expected: FAIL because the workspace still exposes `aim`, `aim-cli`, `aim-core`, and `aim` default paths. + +**Step 3: Perform the crate and manifest rename** + +Run: + +```bash +git mv crates/aim-cli crates/upm +git mv crates/aim-core crates/upm-core +``` + +Then update: + +- workspace members and default members in `Cargo.toml` +- package names to `upm` and `upm-core` +- binary name to `upm` +- crate imports from `aim_core` to `upm_core` +- crate imports from `aim_cli` to `upm` +- clap command name from `aim` to `upm` +- default config, registry, payload-root, and desktop-entry paths to `upm` + +**Step 4: Run the focused tests to verify pass** + +Run: + +```bash +cargo test --package upm --test cli_smoke +cargo test --package upm --test cli_commands +cargo test --package upm --test config_loading +cargo test --package upm-core --test install_paths +cargo test --package upm-core --test install_policy +``` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add Cargo.toml crates/upm crates/upm-core +git commit -m "refactor: rename workspace to upm" +``` + +### Task 2: Remove remaining `aim`-named runtime interfaces + +**Files:** +- Modify: `crates/upm/src/config.rs` +- Modify: `crates/upm/src/cli/config.rs` +- Modify: `crates/upm/src/lib.rs` +- Modify: `crates/upm/src/ui/prompt.rs` +- Modify: `crates/upm-core/src/platform/mod.rs` +- Modify: `crates/upm-core/src/source/github.rs` +- Modify: `crates/upm-core/src/source/appimagehub.rs` +- Modify: `crates/upm-core/src/integration/refresh.rs` +- Test: `crates/upm/tests/config_loading.rs` + +**Step 1: Write the failing strict-rename expectations** + +Update representative tests to cover: + +- config lookup uses `UPM_CONFIG_PATH` +- registry lookup uses `UPM_REGISTRY_PATH` +- old `AIM_*` config and registry overrides are ignored +- tracking preference uses `UPM_TRACKING_PREFERENCE` +- old `AIM_TRACKING_PREFERENCE` is ignored +- provider fixture execution uses the renamed `UPM_*` interfaces through CLI-facing tests +- managed install and summary output use `upm` paths and desktop prefixes + +**Step 2: Run the focused tests to verify failure** + +Run: + +```bash +cargo test --package upm --test config_loading +cargo test --package upm --test search_cli +cargo test --package upm --test end_to_end_cli +cargo test --package upm --test ui_summary +``` + +Expected: FAIL because representative CLI and config flows still depend on old `aim` names. + +**Step 3: Remove the remaining `aim` interfaces** + +Update the codebase so renamed runtime interfaces are consistently `upm`: + +- environment variable names use `UPM_*` +- helper/debug prefixes print `[upm]` +- GitHub user agent identifies as `upm/0.1` +- old `aim` compatibility reads are removed instead of preserved + +**Step 4: Run the focused tests to verify pass** + +Run: + +```bash +cargo test --package upm --test config_loading +cargo test --package upm --test search_cli +cargo test --package upm --test end_to_end_cli +cargo test --package upm --test ui_summary +``` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add crates/upm/src/config.rs crates/upm/src/cli/config.rs crates/upm/src/lib.rs crates/upm/src/ui/prompt.rs crates/upm-core/src/platform/mod.rs crates/upm-core/src/source/github.rs crates/upm-core/src/source/appimagehub.rs crates/upm-core/src/integration/refresh.rs crates/upm/tests/config_loading.rs +git commit -m "refactor: remove remaining aim runtime interfaces" +``` + +### Task 3: Add a provider-composition seam in `upm-core` + +**Files:** +- Create: `crates/upm-core/src/app/providers.rs` +- Modify: `crates/upm-core/src/app/mod.rs` +- Modify: `crates/upm-core/src/app/add.rs` +- Modify: `crates/upm-core/src/app/search.rs` +- Modify: `crates/upm-core/src/lib.rs` +- Create: `crates/upm-core/tests/provider_registry.rs` + +**Step 1: Write the failing provider-composition tests** + +Create `crates/upm-core/tests/provider_registry.rs` with two focused tests: + +- `build_search_results_with_registered_providers_uses_external_hits` using a stub external search provider +- `build_add_plan_with_registered_providers_delegates_appimagehub_like_sources` using a stub external add provider that returns a fixed artifact and release + +The tests should prove that `upm-core` orchestration can consume provider-supplied search and add behavior without hardcoding AppImage-specific modules. + +**Step 2: Run the focused tests to verify failure** + +Run: + +```bash +cargo test --package upm-core --test provider_registry +``` + +Expected: FAIL because the orchestration layer still hardcodes AppImageHub in `app/add.rs` and `app/search.rs`. + +**Step 3: Introduce the narrow composition API** + +Create `crates/upm-core/src/app/providers.rs` with minimal types: + +- `pub trait ExternalAddProvider` +- `pub struct ExternalAddResolution` +- `pub struct ProviderRegistry<'a>` + +Requirements: + +- `ProviderRegistry` carries `search_providers: Vec<&'a dyn SearchProvider>` and `external_add_providers: Vec<&'a dyn ExternalAddProvider>` +- `build_search_results` can delegate to `build_search_results_with` using providers supplied by the caller +- `build_add_plan_with_reporter_and_policy` gets a sibling entrypoint that accepts a `ProviderRegistry` +- core built-ins remain in `upm-core`; only AppImage-specific exact-resolution and search logic should move behind the new registry seam + +Keep the interface intentionally small. Do not attempt plugin loading or dynamic discovery yet. + +**Step 4: Run the focused tests to verify pass** + +Run: + +```bash +cargo test --package upm-core --test provider_registry +``` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add crates/upm-core/src/app/providers.rs crates/upm-core/src/app/mod.rs crates/upm-core/src/app/add.rs crates/upm-core/src/app/search.rs crates/upm-core/src/lib.rs crates/upm-core/tests/provider_registry.rs +git commit -m "refactor: add provider composition seam to upm-core" +``` + +### Task 4: Extract AppImage-specific logic into `upm-appimage` + +**Files:** +- Modify: `Cargo.toml` +- Create: `crates/upm-appimage/Cargo.toml` +- Create: `crates/upm-appimage/src/lib.rs` +- Create: `crates/upm-appimage/src/add.rs` +- Create: `crates/upm-appimage/src/search.rs` +- Create: `crates/upm-appimage/src/source/mod.rs` +- Create: `crates/upm-appimage/src/source/appimagehub.rs` +- Modify: `crates/upm-core/src/adapters/mod.rs` +- Modify: `crates/upm-core/src/source/mod.rs` +- Modify: `crates/upm-core/src/app/add.rs` +- Modify: `crates/upm-core/src/app/search.rs` +- Create: `crates/upm-appimage/tests/appimagehub_search.rs` +- Modify: `crates/upm-core/tests/adapter_contract.rs` +- Modify: `crates/upm-core/tests/adapter_smoke.rs` + +**Step 1: Write the failing extracted-module test** + +Create `crates/upm-appimage/tests/appimagehub_search.rs` by moving the current AppImageHub search expectations out of `upm-core` and updating imports to target the new crate. + +Also update the affected `upm-core` tests so they no longer import `AppImageHubAdapter` from `upm-core` directly. + +**Step 2: Run the focused tests to verify failure** + +Run: + +```bash +cargo test --package upm-appimage --test appimagehub_search +``` + +Expected: FAIL because the new crate does not exist yet. + +**Step 3: Create the new crate and move AppImageHub implementation into it** + +Move the AppImageHub-specific code into the new crate: + +- transport and fixture logic from `upm-core/src/source/appimagehub.rs` +- AppImage-backed exact-resolution logic into `crates/upm-appimage/src/add.rs` implementing `ExternalAddProvider` +- AppImageHub search provider logic out of `upm-core/src/app/search.rs` into `crates/upm-appimage/src/search.rs` + +`upm-appimage` should depend on `upm-core`, not the other way around. + +Leave `SourceKind::AppImageHub`, `SourceInputKind::AppImageHub*`, and `NormalizedSourceKind::AppImageHub` in `upm-core` for this milestone. The deeper provider/domain generalization belongs to the next milestone. + +**Step 4: Remove direct AppImageHub wiring from `upm-core`** + +Update `upm-core` so it no longer declares: + +- `pub mod appimagehub;` in `src/adapters/mod.rs` +- `pub mod appimagehub;` in `src/source/mod.rs` +- built-in AppImageHub search-provider construction in `src/app/search.rs` +- direct `AppImageHubAdapter` imports in `src/app/add.rs` + +After this step, AppImage behavior should exist only through the provider registry seam from Task 3. + +**Step 5: Run the focused tests to verify pass** + +Run: + +```bash +cargo test --package upm-appimage --test appimagehub_search +cargo test --package upm-core --test adapter_contract +cargo test --package upm-core --test adapter_smoke +``` + +Expected: PASS. + +**Step 6: Commit** + +```bash +git add Cargo.toml crates/upm-appimage crates/upm-core/src/adapters/mod.rs crates/upm-core/src/source/mod.rs crates/upm-core/src/app/add.rs crates/upm-core/src/app/search.rs crates/upm-core/tests/adapter_contract.rs crates/upm-core/tests/adapter_smoke.rs +git commit -m "refactor: extract appimage support into upm-appimage" +``` + +### Task 5: Rewire the `upm` CLI to assemble built-in providers from modules + +**Files:** +- Modify: `crates/upm/Cargo.toml` +- Create: `crates/upm/src/providers.rs` +- Modify: `crates/upm/src/lib.rs` +- Test: `crates/upm/tests/search_cli.rs` +- Test: `crates/upm/tests/end_to_end_cli.rs` +- Test: `crates/upm/tests/ui_summary.rs` + +**Step 1: Write the failing CLI integration expectations** + +Update CLI integration tests to prove that: + +- `upm search firefox` still includes AppImageHub results +- direct `upm appimagehub/2338455` install flow still succeeds through the CLI +- the final summary output still renders the new `upm`-prefixed paths and desktop-entry names + +**Step 2: Run the focused tests to verify failure** + +Run: + +```bash +cargo test --package upm --test search_cli +cargo test --package upm --test end_to_end_cli +cargo test --package upm --test ui_summary +``` + +Expected: FAIL because `upm` does not yet assemble AppImage providers through the extracted module. + +**Step 3: Add CLI-side provider assembly** + +Create `crates/upm/src/providers.rs` that: + +- builds the `ProviderRegistry` for `upm-core` +- registers the `upm-appimage` search provider +- registers the `upm-appimage` external add provider + +Update `crates/upm/src/lib.rs` so dispatch paths call the provider-aware core entrypoints instead of hardcoded core defaults. + +Do not move progress rendering or config loading into `upm-core`; the CLI remains the presentation layer. + +**Step 4: Run the focused tests to verify pass** + +Run: + +```bash +cargo test --package upm --test search_cli +cargo test --package upm --test end_to_end_cli +cargo test --package upm --test ui_summary +``` + +Expected: PASS. + +**Step 5: Commit** + +```bash +git add crates/upm/Cargo.toml crates/upm/src/providers.rs crates/upm/src/lib.rs crates/upm/tests/search_cli.rs crates/upm/tests/end_to_end_cli.rs crates/upm/tests/ui_summary.rs +git commit -m "refactor: compose providers from upm modules" +``` + +### Task 6: Update docs and run full workspace verification + +**Files:** +- Modify: `README.md` +- Modify: `.architecture/overview.md` +- Modify: `.architecture/roadmap.md` + +**Step 1: Update product and architecture docs** + +Document: + +- the workspace rename to `upm` +- `upm-core` as the headless application layer +- `upm-appimage` as the first installable provider module +- compatibility behavior for existing `aim` config and registry locations +- the fact that provider composition now happens in the CLI rather than through hardcoded AppImage paths in `upm-core` + +**Step 2: Verify the docs mention the new structure** + +Run: + +```bash +rg -n "upm-core|upm-appimage|legacy aim|ProviderRegistry|upm" README.md .architecture/overview.md .architecture/roadmap.md +``` + +Expected: matches showing the renamed crates, provider split, and compatibility note. + +**Step 3: Run the full verification suite** + +Run: + +```bash +cargo fmt --all +cargo test --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +``` + +Expected: PASS. + +**Step 4: Commit** + +```bash +git add README.md .architecture/overview.md .architecture/roadmap.md +git commit -m "docs: describe upm core and module split" +``` \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index f7cdfcd..c8956f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,4 +10,8 @@ Audits are to live in `.audits` with a good name slug plus time & date. IMPORTANT TO CHECK BEFORE ANY COMMIT!! Architecture under `.architecture` must be maintained with each change. - Security issues stumbled upon or noticed during execution **already in code** must live in `security-issues.md`. Newly added issues during execution or planning should be raised to the user and/or dealt with, instead of growing the list. - - An overview of the workspace, should live in `overview.md`. \ No newline at end of file + - An overview of the workspace, should live in `overview.md`. + - A roadmap of the planned direction & features in `roadmap.md`. + + ## UPM work + We are currently using the `upm` branch as our "main" for all `upm` work, meaning feature branches/worktrees will merge back into here. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d60c173..5130bcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,41 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aim-cli" -version = "0.1.0" -dependencies = [ - "aim-core", - "assert_cmd", - "clap", - "console 0.16.3", - "crossterm", - "dialoguer", - "indicatif", - "libc", - "predicates", - "ratatui", - "serde", - "tempfile", - "toml", -] - -[[package]] -name = "aim-core" -version = "0.1.0" -dependencies = [ - "base64", - "fs2", - "md5", - "quick-xml", - "reqwest", - "serde", - "serde_yaml", - "sha2", - "tempfile", - "toml", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -1966,6 +1931,52 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "upm" +version = "0.1.0" +dependencies = [ + "assert_cmd", + "clap", + "console 0.16.3", + "crossterm", + "dialoguer", + "indicatif", + "libc", + "predicates", + "ratatui", + "serde", + "tempfile", + "toml", + "upm-appimage", + "upm-core", +] + +[[package]] +name = "upm-appimage" +version = "0.1.0" +dependencies = [ + "quick-xml", + "reqwest", + "serde", + "upm-core", +] + +[[package]] +name = "upm-core" +version = "0.1.0" +dependencies = [ + "base64", + "fs2", + "md5", + "quick-xml", + "reqwest", + "serde", + "serde_yaml", + "sha2", + "tempfile", + "toml", +] + [[package]] name = "url" version = "2.5.8" diff --git a/Cargo.toml b/Cargo.toml index a1dc9cb..847258b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,11 @@ [workspace] members = [ - "crates/aim-core", - "crates/aim-cli", + "crates/upm-core", + "crates/upm-appimage", + "crates/upm", ] default-members = [ - "crates/aim-cli", + "crates/upm", ] resolver = "2" diff --git a/README.md b/README.md index 9a056ac..d8c15c7 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,25 @@ -# aim -AppImage Manager +# upm +Universal Package Manager -`aim` is a Rust Cargo workspace for managing AppImages from multiple source types. +`upm` is a Rust Cargo workspace for a modular package manager with a shared headless core and provider crates. ## Workspace -- `crates/aim-core`: business logic, source adapters, registry, install/update planning -- `crates/aim-cli`: thin terminal frontend for parsing, prompting, and rendering +- `crates/upm-core`: headless application layer for query normalization, resolution, planning, registry persistence, install/update orchestration, and provider-facing APIs +- `crates/upm`: thin terminal frontend for argument parsing, config loading, prompting, progress reporting, and summary rendering +- `crates/upm-appimage`: AppImageHub transport, search, and add-provider integration composed into the CLI through `ProviderRegistry` -The split is intentional so a future GUI client can reuse `aim-core` without moving logic out of the shared library. +The split is intentional so future frontends can reuse `upm-core`, while package-source behavior stays modular instead of being hardcoded into the core. ## Commands ```text -aim -aim -aim update -aim list -aim search -aim remove +upm +upm +upm update +upm list +upm search +upm remove ``` ## Query Forms @@ -36,22 +37,22 @@ aim remove ## Search -`aim search ` is part of v0.9 finalisation. +`upm search ` is part of the initial modular provider surface. - search is provider-extensible and currently includes GitHub plus AppImageHub - search results should resolve to install-ready queries such as `owner/repo` and `appimagehub/` -- the search model is provider-extensible for future phases +- provider composition happens in `crates/upm/src/providers.rs`, not through AppImageHub-specific wiring inside `upm-core` ## Scope Overrides -By default `aim` auto-detects whether to use user or system scope. Override that with: +By default `upm` auto-detects whether to use user or system scope. Override that with: - `--user` - `--system` ## Config -Runtime config is loaded from `~/.config/aim/config.toml` or `$XDG_CONFIG_HOME/aim/config.toml`. +Runtime config is loaded from `~/.config/upm/config.toml` or `$XDG_CONFIG_HOME/upm/config.toml`. Example: @@ -63,13 +64,20 @@ allow_http = true - `allow_http` only permits user-supplied `http://` inputs such as direct URL installs or updates from previously installed direct HTTP origins - provider-resolved downloads such as AppImageHub artifacts remain HTTPS-only even when `allow_http = true` +## Breaking Rename + +- `upm` is a hard rename from `aim` +- runtime overrides now use `UPM_*` names such as `UPM_CONFIG_PATH` and `UPM_REGISTRY_PATH` +- old `AIM_*` runtime overrides are intentionally ignored +- default config and registry locations now live under `upm` paths + ## Current Flow Shape -- `aim ` installs direct provider matches when available, otherwise falls back to search results, shows live progress on stderr, prints an `Installation Summary` on stdout for installs, and renders an `Installation Review` when tracking needs confirmation -- bare `aim` prints an `Update Review` without mutating the registry -- `aim update` executes the pending updates, streams live status on stderr, then prints an `Update Summary` -- `aim list` renders either `Installed Apps` or `No installed apps yet` -- `aim remove ` resolves a registered application name, streams removal progress on stderr, then prints a `Removal Summary` +- `upm ` installs direct provider matches when available, otherwise falls back to search results, shows live progress on stderr, prints an `Installation Summary` on stdout for installs, and renders an `Installation Review` when tracking needs confirmation +- bare `upm` prints an `Update Review` without mutating the registry +- `upm update` executes the pending updates, streams live status on stderr, then prints an `Update Summary` +- `upm list` renders either `Installed Apps` or `No installed apps yet` +- `upm remove ` resolves a registered application name, streams removal progress on stderr, then prints a `Removal Summary` ## Terminal UX diff --git a/crates/aim-cli/tests/config_loading.rs b/crates/aim-cli/tests/config_loading.rs deleted file mode 100644 index cbf13e7..0000000 --- a/crates/aim-cli/tests/config_loading.rs +++ /dev/null @@ -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:?}"), - } -} diff --git a/crates/aim-core/tests/install_paths.rs b/crates/aim-core/tests/install_paths.rs deleted file mode 100644 index 217e29e..0000000 --- a/crates/aim-core/tests/install_paths.rs +++ /dev/null @@ -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")); -} diff --git a/crates/upm-appimage/Cargo.toml b/crates/upm-appimage/Cargo.toml new file mode 100644 index 0000000..8761a15 --- /dev/null +++ b/crates/upm-appimage/Cargo.toml @@ -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" } \ No newline at end of file diff --git a/crates/upm-appimage/src/add.rs b/crates/upm-appimage/src/add.rs new file mode 100644 index 0000000..a374254 --- /dev/null +++ b/crates/upm-appimage/src/add.rs @@ -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( + &self, + source: &SourceRef, + transport: &T, + ) -> Result { + 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 { + Some(SourceKind::AppImageHub) + } + + fn normalize(&self, query: &str) -> Result { + 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 { + 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 { + 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 ExternalAddProvider for AppImageHubAddProvider<'_, T> { + fn id(&self) -> &'static str { + "appimagehub" + } + + fn resolve(&self, source: &SourceRef) -> Result, 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}") + } + } +} diff --git a/crates/upm-appimage/src/lib.rs b/crates/upm-appimage/src/lib.rs new file mode 100644 index 0000000..a226e3f --- /dev/null +++ b/crates/upm-appimage/src/lib.rs @@ -0,0 +1,6 @@ +pub mod add; +pub mod search; +pub mod source; + +pub use add::{AppImageHubAdapter, AppImageHubAddProvider}; +pub use search::AppImageHubSearchProvider; diff --git a/crates/upm-appimage/src/search.rs b/crates/upm-appimage/src/search.rs new file mode 100644 index 0000000..8160f71 --- /dev/null +++ b/crates/upm-appimage/src/search.rs @@ -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 SearchProvider for AppImageHubSearchProvider<'_, T> { + fn search(&self, query: &SearchQuery) -> Result, 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::>(); + + 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(), + } +} diff --git a/crates/upm-appimage/src/source/appimagehub.rs b/crates/upm-appimage/src/source/appimagehub.rs new file mode 100644 index 0000000..165a65f --- /dev/null +++ b/crates/upm-appimage/src/source/appimagehub.rs @@ -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, + pub arch: Option, + pub md5sum: Option, + pub version: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AppImageHubItem { + pub id: String, + pub name: String, + pub version: String, + pub summary: Option, + pub detail_page: String, + pub tags: Vec, + pub downloads: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AppImageHubSearchHit { + pub id: String, + pub name: String, + pub version: String, + pub summary: Option, + pub detail_page: String, + pub tags: Vec, +} + +#[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; + + fn search_items( + &self, + query: &str, + limit: usize, + ) -> Result, AppImageHubSearchError>; +} + +pub fn default_transport() -> Box { + 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, AppImageHubError> { + let transport = default_transport(); + resolve_appimagehub_item_with(source, transport.as_ref()) +} + +pub fn resolve_appimagehub_item_with( + source: &SourceRef, + transport: &T, +) -> Result, 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, AppImageHubSearchError> { + let transport = default_transport(); + search_appimagehub_with(query, limit, transport.as_ref()) +} + +pub fn search_appimagehub_with( + query: &str, + limit: usize, + transport: &T, +) -> Result, 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 { + 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, 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 { + fixture_item(id).ok_or_else(|| AppImageHubError::FixtureItemMissing(id.to_owned())) + } + + fn search_items( + &self, + query: &str, + limit: usize, + ) -> Result, 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, +} + +#[derive(serde::Deserialize)] +struct OcsContent { + id: String, + name: String, + version: Option, + summary: Option, + detailpage: Option, + tags: Option, + downloadlink1: Option, + downloadname1: Option, + download_package_type1: Option, + download_package_arch1: Option, + downloadmd5sum1: Option, + download_version1: Option, + downloadlink2: Option, + downloadname2: Option, + download_package_type2: Option, + download_package_arch2: Option, + downloadmd5sum2: Option, + download_version2: Option, + downloadlink3: Option, + downloadname3: Option, + download_package_type3: Option, + download_package_arch3: Option, + downloadmd5sum3: Option, + download_version3: Option, +} + +fn parse_item_xml(xml: &str) -> Result { + let parsed = + quick_xml::de::from_str::(xml).map_err(AppImageHubError::Parse)?; + Ok(content_to_item(parsed.data.content)) +} + +fn parse_search_xml(xml: &str) -> Result, AppImageHubSearchError> { + if !xml.contains("") { + return Ok(Vec::new()); + } + + let parsed = + quick_xml::de::from_str::(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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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() +} diff --git a/crates/upm-appimage/src/source/mod.rs b/crates/upm-appimage/src/source/mod.rs new file mode 100644 index 0000000..f23d1b0 --- /dev/null +++ b/crates/upm-appimage/src/source/mod.rs @@ -0,0 +1 @@ +pub mod appimagehub; diff --git a/crates/aim-core/tests/appimagehub_search.rs b/crates/upm-appimage/tests/appimagehub_search.rs similarity index 63% rename from crates/aim-core/tests/appimagehub_search.rs rename to crates/upm-appimage/tests/appimagehub_search.rs index 7168e0d..3ccebe5 100644 --- a/crates/aim-core/tests/appimagehub_search.rs +++ b/crates/upm-appimage/tests/appimagehub_search.rs @@ -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") + ); +} diff --git a/crates/aim-core/Cargo.toml b/crates/upm-core/Cargo.toml similarity index 95% rename from crates/aim-core/Cargo.toml rename to crates/upm-core/Cargo.toml index 73716b6..7f483e6 100644 --- a/crates/aim-core/Cargo.toml +++ b/crates/upm-core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "aim-core" +name = "upm-core" version.workspace = true edition.workspace = true license.workspace = true diff --git a/crates/aim-core/src/adapters/appimagehub.rs b/crates/upm-core/src/adapters/appimagehub.rs similarity index 100% rename from crates/aim-core/src/adapters/appimagehub.rs rename to crates/upm-core/src/adapters/appimagehub.rs diff --git a/crates/aim-core/src/adapters/direct_url.rs b/crates/upm-core/src/adapters/direct_url.rs similarity index 100% rename from crates/aim-core/src/adapters/direct_url.rs rename to crates/upm-core/src/adapters/direct_url.rs diff --git a/crates/aim-core/src/adapters/github.rs b/crates/upm-core/src/adapters/github.rs similarity index 100% rename from crates/aim-core/src/adapters/github.rs rename to crates/upm-core/src/adapters/github.rs diff --git a/crates/aim-core/src/adapters/gitlab.rs b/crates/upm-core/src/adapters/gitlab.rs similarity index 100% rename from crates/aim-core/src/adapters/gitlab.rs rename to crates/upm-core/src/adapters/gitlab.rs diff --git a/crates/aim-core/src/adapters/mod.rs b/crates/upm-core/src/adapters/mod.rs similarity index 75% rename from crates/aim-core/src/adapters/mod.rs rename to crates/upm-core/src/adapters/mod.rs index 2d25232..15a0aba 100644 --- a/crates/aim-core/src/adapters/mod.rs +++ b/crates/upm-core/src/adapters/mod.rs @@ -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(adapter: &A, source: &SourceRef) -> bool { diff --git a/crates/aim-core/src/adapters/sourceforge.rs b/crates/upm-core/src/adapters/sourceforge.rs similarity index 100% rename from crates/aim-core/src/adapters/sourceforge.rs rename to crates/upm-core/src/adapters/sourceforge.rs diff --git a/crates/aim-core/src/adapters/test_support.rs b/crates/upm-core/src/adapters/test_support.rs similarity index 100% rename from crates/aim-core/src/adapters/test_support.rs rename to crates/upm-core/src/adapters/test_support.rs diff --git a/crates/aim-core/src/adapters/traits.rs b/crates/upm-core/src/adapters/traits.rs similarity index 100% rename from crates/aim-core/src/adapters/traits.rs rename to crates/upm-core/src/adapters/traits.rs diff --git a/crates/aim-core/src/adapters/zsync.rs b/crates/upm-core/src/adapters/zsync.rs similarity index 100% rename from crates/aim-core/src/adapters/zsync.rs rename to crates/upm-core/src/adapters/zsync.rs diff --git a/crates/aim-core/src/app/add.rs b/crates/upm-core/src/app/add.rs similarity index 87% rename from crates/aim-core/src/app/add.rs rename to crates/upm-core/src/app/add.rs index fcf517a..69e4276 100644 --- a/crates/aim-core/src/app/add.rs +++ b/crates/upm-core/src/app/add.rs @@ -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 { 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( transport: &T, ) -> Result { 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( transport: &T, reporter: &mut impl ProgressReporter, ) -> Result { - 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( + query: &str, + transport: &T, + providers: &ProviderRegistry<'_>, + policy: AddSecurityPolicy, +) -> Result { + 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( + query: &str, + transport: &T, + reporter: &mut impl ProgressReporter, + providers: &ProviderRegistry<'_>, + policy: AddSecurityPolicy, +) -> Result { + 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( transport: &T, reporter: &mut impl ProgressReporter, policy: AddSecurityPolicy, +) -> Result { + 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( + query: &str, + transport: &T, + reporter: &mut impl ProgressReporter, + policy: AddSecurityPolicy, + providers: &ProviderRegistry<'_>, ) -> Result { reporter.report(&OperationEvent::StageChanged { stage: OperationStage::ResolveQuery, @@ -91,8 +138,7 @@ pub fn build_add_plan_with_reporter_and_policy( 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( }, artifact, strategy, + None, ) } SourceKind::GitLab => { @@ -188,59 +235,29 @@ pub fn build_add_plan_with_reporter_and_policy( 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( 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( 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( }, alternates: Vec::new(), }; - (resolution, artifact, strategy) + (resolution, artifact, strategy, None) } }; @@ -359,6 +376,21 @@ pub fn build_add_plan_with_reporter_and_policy( }) } +fn resolve_registered_external_add_provider( + source: &crate::domain::source::SourceRef, + providers: &ProviderRegistry<'_>, +) -> Result, 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 { 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(( diff --git a/crates/aim-core/src/app/identity.rs b/crates/upm-core/src/app/identity.rs similarity index 100% rename from crates/aim-core/src/app/identity.rs rename to crates/upm-core/src/app/identity.rs diff --git a/crates/aim-core/src/app/interaction.rs b/crates/upm-core/src/app/interaction.rs similarity index 100% rename from crates/aim-core/src/app/interaction.rs rename to crates/upm-core/src/app/interaction.rs diff --git a/crates/aim-core/src/app/list.rs b/crates/upm-core/src/app/list.rs similarity index 100% rename from crates/aim-core/src/app/list.rs rename to crates/upm-core/src/app/list.rs diff --git a/crates/aim-core/src/app/mod.rs b/crates/upm-core/src/app/mod.rs similarity index 90% rename from crates/aim-core/src/app/mod.rs rename to crates/upm-core/src/app/mod.rs index 2d46ab8..0e632e2 100644 --- a/crates/aim-core/src/app/mod.rs +++ b/crates/upm-core/src/app/mod.rs @@ -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; diff --git a/crates/aim-core/src/app/progress.rs b/crates/upm-core/src/app/progress.rs similarity index 100% rename from crates/aim-core/src/app/progress.rs rename to crates/upm-core/src/app/progress.rs diff --git a/crates/upm-core/src/app/providers.rs b/crates/upm-core/src/app/providers.rs new file mode 100644 index 0000000..c703e2e --- /dev/null +++ b/crates/upm-core/src/app/providers.rs @@ -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, 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, +} + +#[derive(Default)] +pub struct ProviderRegistry<'a> { + pub search_providers: Vec<&'a dyn SearchProvider>, + pub external_add_providers: Vec<&'a dyn ExternalAddProvider>, +} diff --git a/crates/aim-core/src/app/query.rs b/crates/upm-core/src/app/query.rs similarity index 100% rename from crates/aim-core/src/app/query.rs rename to crates/upm-core/src/app/query.rs diff --git a/crates/aim-core/src/app/remove.rs b/crates/upm-core/src/app/remove.rs similarity index 100% rename from crates/aim-core/src/app/remove.rs rename to crates/upm-core/src/app/remove.rs diff --git a/crates/aim-core/src/app/scope.rs b/crates/upm-core/src/app/scope.rs similarity index 100% rename from crates/aim-core/src/app/scope.rs rename to crates/upm-core/src/app/scope.rs diff --git a/crates/aim-core/src/app/search.rs b/crates/upm-core/src/app/search.rs similarity index 77% rename from crates/aim-core/src/app/search.rs rename to crates/upm-core/src/app/search.rs index fd396ea..5928a28 100644 --- a/crates/aim-core/src/app/search.rs +++ b/crates/upm-core/src/app/search.rs @@ -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 { - 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 { + 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 SearchProvider for AppImageHubSearchProvider<'_, T> { - fn search(&self, query: &SearchQuery) -> Result, 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::>(); - - 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 SearchProvider for GitHubSearchProvider<'_, T> { fn search(&self, query: &SearchQuery) -> Result, 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(), - } -} diff --git a/crates/aim-core/src/app/show.rs b/crates/upm-core/src/app/show.rs similarity index 100% rename from crates/aim-core/src/app/show.rs rename to crates/upm-core/src/app/show.rs diff --git a/crates/aim-core/src/app/update.rs b/crates/upm-core/src/app/update.rs similarity index 99% rename from crates/aim-core/src/app/update.rs rename to crates/upm-core/src/app/update.rs index dfb5b48..4333f56 100644 --- a/crates/aim-core/src/app/update.rs +++ b/crates/upm-core/src/app/update.rs @@ -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}"))?; diff --git a/crates/aim-core/src/domain/app.rs b/crates/upm-core/src/domain/app.rs similarity index 100% rename from crates/aim-core/src/domain/app.rs rename to crates/upm-core/src/domain/app.rs diff --git a/crates/aim-core/src/domain/mod.rs b/crates/upm-core/src/domain/mod.rs similarity index 100% rename from crates/aim-core/src/domain/mod.rs rename to crates/upm-core/src/domain/mod.rs diff --git a/crates/aim-core/src/domain/search.rs b/crates/upm-core/src/domain/search.rs similarity index 100% rename from crates/aim-core/src/domain/search.rs rename to crates/upm-core/src/domain/search.rs diff --git a/crates/aim-core/src/domain/show.rs b/crates/upm-core/src/domain/show.rs similarity index 100% rename from crates/aim-core/src/domain/show.rs rename to crates/upm-core/src/domain/show.rs diff --git a/crates/aim-core/src/domain/source.rs b/crates/upm-core/src/domain/source.rs similarity index 100% rename from crates/aim-core/src/domain/source.rs rename to crates/upm-core/src/domain/source.rs diff --git a/crates/aim-core/src/domain/update.rs b/crates/upm-core/src/domain/update.rs similarity index 100% rename from crates/aim-core/src/domain/update.rs rename to crates/upm-core/src/domain/update.rs diff --git a/crates/aim-core/src/integration/desktop.rs b/crates/upm-core/src/integration/desktop.rs similarity index 100% rename from crates/aim-core/src/integration/desktop.rs rename to crates/upm-core/src/integration/desktop.rs diff --git a/crates/aim-core/src/integration/install.rs b/crates/upm-core/src/integration/install.rs similarity index 100% rename from crates/aim-core/src/integration/install.rs rename to crates/upm-core/src/integration/install.rs diff --git a/crates/aim-core/src/integration/mod.rs b/crates/upm-core/src/integration/mod.rs similarity index 100% rename from crates/aim-core/src/integration/mod.rs rename to crates/upm-core/src/integration/mod.rs diff --git a/crates/aim-core/src/integration/paths.rs b/crates/upm-core/src/integration/paths.rs similarity index 95% rename from crates/aim-core/src/integration/paths.rs rename to crates/upm-core/src/integration/paths.rs index 8cd7c16..2851079 100644 --- a/crates/aim-core/src/integration/paths.rs +++ b/crates/upm-core/src/integration/paths.rs @@ -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 { diff --git a/crates/aim-core/src/integration/policy.rs b/crates/upm-core/src/integration/policy.rs similarity index 94% rename from crates/aim-core/src/integration/policy.rs rename to crates/upm-core/src/integration/policy.rs index fb99531..2fe80db 100644 --- a/crates/aim-core/src/integration/policy.rs +++ b/crates/upm-core/src/integration/policy.rs @@ -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 { diff --git a/crates/aim-core/src/integration/refresh.rs b/crates/upm-core/src/integration/refresh.rs similarity index 86% rename from crates/aim-core/src/integration/refresh.rs rename to crates/upm-core/src/integration/refresh.rs index 740373b..c4ebc69 100644 --- a/crates/aim-core/src/integration/refresh.rs +++ b/crates/upm-core/src/integration/refresh.rs @@ -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::>() .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) { - 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()); } diff --git a/crates/aim-core/src/lib.rs b/crates/upm-core/src/lib.rs similarity index 63% rename from crates/aim-core/src/lib.rs rename to crates/upm-core/src/lib.rs index 4a32a3f..60b0706 100644 --- a/crates/aim-core/src/lib.rs +++ b/crates/upm-core/src/lib.rs @@ -7,3 +7,5 @@ pub mod platform; pub mod registry; pub mod source; pub mod update; + +pub use app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry}; diff --git a/crates/aim-core/src/metadata/document.rs b/crates/upm-core/src/metadata/document.rs similarity index 100% rename from crates/aim-core/src/metadata/document.rs rename to crates/upm-core/src/metadata/document.rs diff --git a/crates/aim-core/src/metadata/electron_builder.rs b/crates/upm-core/src/metadata/electron_builder.rs similarity index 100% rename from crates/aim-core/src/metadata/electron_builder.rs rename to crates/upm-core/src/metadata/electron_builder.rs diff --git a/crates/aim-core/src/metadata/mod.rs b/crates/upm-core/src/metadata/mod.rs similarity index 100% rename from crates/aim-core/src/metadata/mod.rs rename to crates/upm-core/src/metadata/mod.rs diff --git a/crates/aim-core/src/metadata/parser.rs b/crates/upm-core/src/metadata/parser.rs similarity index 100% rename from crates/aim-core/src/metadata/parser.rs rename to crates/upm-core/src/metadata/parser.rs diff --git a/crates/aim-core/src/metadata/zsync.rs b/crates/upm-core/src/metadata/zsync.rs similarity index 100% rename from crates/aim-core/src/metadata/zsync.rs rename to crates/upm-core/src/metadata/zsync.rs diff --git a/crates/aim-core/src/platform/capabilities.rs b/crates/upm-core/src/platform/capabilities.rs similarity index 97% rename from crates/aim-core/src/platform/capabilities.rs rename to crates/upm-core/src/platform/capabilities.rs index 238bfc0..bdaab79 100644 --- a/crates/aim-core/src/platform/capabilities.rs +++ b/crates/upm-core/src/platform/capabilities.rs @@ -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) diff --git a/crates/aim-core/src/platform/distro.rs b/crates/upm-core/src/platform/distro.rs similarity index 100% rename from crates/aim-core/src/platform/distro.rs rename to crates/upm-core/src/platform/distro.rs diff --git a/crates/aim-core/src/platform/mod.rs b/crates/upm-core/src/platform/mod.rs similarity index 93% rename from crates/aim-core/src/platform/mod.rs rename to crates/upm-core/src/platform/mod.rs index 4636245..4646924 100644 --- a/crates/aim-core/src/platform/mod.rs +++ b/crates/upm-core/src/platform/mod.rs @@ -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 { diff --git a/crates/aim-core/src/registry/mod.rs b/crates/upm-core/src/registry/mod.rs similarity index 100% rename from crates/aim-core/src/registry/mod.rs rename to crates/upm-core/src/registry/mod.rs diff --git a/crates/aim-core/src/registry/model.rs b/crates/upm-core/src/registry/model.rs similarity index 100% rename from crates/aim-core/src/registry/model.rs rename to crates/upm-core/src/registry/model.rs diff --git a/crates/aim-core/src/registry/store.rs b/crates/upm-core/src/registry/store.rs similarity index 100% rename from crates/aim-core/src/registry/store.rs rename to crates/upm-core/src/registry/store.rs diff --git a/crates/aim-core/src/source/appimagehub.rs b/crates/upm-core/src/source/appimagehub.rs similarity index 97% rename from crates/aim-core/src/source/appimagehub.rs rename to crates/upm-core/src/source/appimagehub.rs index c0db682..9b64dfc 100644 --- a/crates/aim-core/src/source/appimagehub.rs +++ b/crates/upm-core/src/source/appimagehub.rs @@ -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 { - 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 { - 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 { diff --git a/crates/aim-core/src/source/github.rs b/crates/upm-core/src/source/github.rs similarity index 97% rename from crates/aim-core/src/source/github.rs rename to crates/upm-core/src/source/github.rs index dec32d4..193f497 100644 --- a/crates/aim-core/src/source/github.rs +++ b/crates/upm-core/src/source/github.rs @@ -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( } pub fn default_transport() -> Box { - 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()), } } diff --git a/crates/aim-core/src/source/input.rs b/crates/upm-core/src/source/input.rs similarity index 100% rename from crates/aim-core/src/source/input.rs rename to crates/upm-core/src/source/input.rs diff --git a/crates/aim-core/src/source/mod.rs b/crates/upm-core/src/source/mod.rs similarity index 59% rename from crates/aim-core/src/source/mod.rs rename to crates/upm-core/src/source/mod.rs index 0b41ca4..72aea24 100644 --- a/crates/aim-core/src/source/mod.rs +++ b/crates/upm-core/src/source/mod.rs @@ -1,3 +1,2 @@ -pub mod appimagehub; pub mod github; pub mod input; diff --git a/crates/aim-core/src/update/channels.rs b/crates/upm-core/src/update/channels.rs similarity index 100% rename from crates/aim-core/src/update/channels.rs rename to crates/upm-core/src/update/channels.rs diff --git a/crates/aim-core/src/update/mod.rs b/crates/upm-core/src/update/mod.rs similarity index 100% rename from crates/aim-core/src/update/mod.rs rename to crates/upm-core/src/update/mod.rs diff --git a/crates/aim-core/src/update/ranking.rs b/crates/upm-core/src/update/ranking.rs similarity index 100% rename from crates/aim-core/src/update/ranking.rs rename to crates/upm-core/src/update/ranking.rs diff --git a/crates/aim-core/tests/adapter_contract.rs b/crates/upm-core/tests/adapter_contract.rs similarity index 86% rename from crates/aim-core/tests/adapter_contract.rs rename to crates/upm-core/tests/adapter_contract.rs index f1f3e14..ad2b38c 100644 --- a/crates/aim-core/tests/adapter_contract.rs +++ b/crates/upm-core/tests/adapter_contract.rs @@ -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(); diff --git a/crates/aim-core/tests/adapter_smoke.rs b/crates/upm-core/tests/adapter_smoke.rs similarity index 79% rename from crates/aim-core/tests/adapter_smoke.rs rename to crates/upm-core/tests/adapter_smoke.rs index 90dad47..40c2fd2 100644 --- a/crates/aim-core/tests/adapter_smoke.rs +++ b/crates/upm-core/tests/adapter_smoke.rs @@ -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")); } diff --git a/crates/aim-core/tests/checksum_verification.rs b/crates/upm-core/tests/checksum_verification.rs similarity index 97% rename from crates/aim-core/tests/checksum_verification.rs rename to crates/upm-core/tests/checksum_verification.rs index b5ab8ce..08e48e5 100644 --- a/crates/aim-core/tests/checksum_verification.rs +++ b/crates/upm-core/tests/checksum_verification.rs @@ -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=="; diff --git a/crates/aim-core/tests/download_pipeline.rs b/crates/upm-core/tests/download_pipeline.rs similarity index 95% rename from crates/aim-core/tests/download_pipeline.rs rename to crates/upm-core/tests/download_pipeline.rs index 3b3eca4..e21d512 100644 --- a/crates/aim-core/tests/download_pipeline.rs +++ b/crates/upm-core/tests/download_pipeline.rs @@ -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() { diff --git a/crates/aim-core/tests/fixtures/example.zsync b/crates/upm-core/tests/fixtures/example.zsync similarity index 100% rename from crates/aim-core/tests/fixtures/example.zsync rename to crates/upm-core/tests/fixtures/example.zsync diff --git a/crates/aim-core/tests/fixtures/latest-linux.yml b/crates/upm-core/tests/fixtures/latest-linux.yml similarity index 100% rename from crates/aim-core/tests/fixtures/latest-linux.yml rename to crates/upm-core/tests/fixtures/latest-linux.yml diff --git a/crates/aim-core/tests/github_add_flow.rs b/crates/upm-core/tests/github_add_flow.rs similarity index 94% rename from crates/aim-core/tests/github_add_flow.rs rename to crates/upm-core/tests/github_add_flow.rs index 37da5c2..8dc3623 100644 --- a/crates/aim-core/tests/github_add_flow.rs +++ b/crates/upm-core/tests/github_add_flow.rs @@ -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() { diff --git a/crates/aim-core/tests/github_source_discovery.rs b/crates/upm-core/tests/github_source_discovery.rs similarity index 94% rename from crates/aim-core/tests/github_source_discovery.rs rename to crates/upm-core/tests/github_source_discovery.rs index 7bc94ac..400b26f 100644 --- a/crates/aim-core/tests/github_source_discovery.rs +++ b/crates/upm-core/tests/github_source_discovery.rs @@ -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() { diff --git a/crates/aim-core/tests/identity_resolution.rs b/crates/upm-core/tests/identity_resolution.rs similarity index 86% rename from crates/aim-core/tests/identity_resolution.rs rename to crates/upm-core/tests/identity_resolution.rs index 24e93cc..564c93c 100644 --- a/crates/aim-core/tests/identity_resolution.rs +++ b/crates/upm-core/tests/identity_resolution.rs @@ -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 ); } diff --git a/crates/aim-core/tests/install_failures.rs b/crates/upm-core/tests/install_failures.rs similarity index 87% rename from crates/aim-core/tests/install_failures.rs rename to crates/upm-core/tests/install_failures.rs index 3f8e6ec..bea996f 100644 --- a/crates/aim-core/tests/install_failures.rs +++ b/crates/upm-core/tests/install_failures.rs @@ -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(); diff --git a/crates/aim-core/tests/install_integration.rs b/crates/upm-core/tests/install_integration.rs similarity index 95% rename from crates/aim-core/tests/install_integration.rs rename to crates/upm-core/tests/install_integration.rs index 8ed1ab1..57eba49 100644 --- a/crates/aim-core/tests/install_integration.rs +++ b/crates/upm-core/tests/install_integration.rs @@ -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 = 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| {}; diff --git a/crates/upm-core/tests/install_paths.rs b/crates/upm-core/tests/install_paths.rs new file mode 100644 index 0000000..72a503d --- /dev/null +++ b/crates/upm-core/tests/install_paths.rs @@ -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")); +} diff --git a/crates/aim-core/tests/install_payload.rs b/crates/upm-core/tests/install_payload.rs similarity index 94% rename from crates/aim-core/tests/install_payload.rs rename to crates/upm-core/tests/install_payload.rs index 37f6278..2e9225e 100644 --- a/crates/aim-core/tests/install_payload.rs +++ b/crates/upm-core/tests/install_payload.rs @@ -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() { diff --git a/crates/aim-core/tests/install_policy.rs b/crates/upm-core/tests/install_policy.rs similarity index 87% rename from crates/aim-core/tests/install_policy.rs rename to crates/upm-core/tests/install_policy.rs index 272bb1f..026127c 100644 --- a/crates/aim-core/tests/install_policy.rs +++ b/crates/upm-core/tests/install_policy.rs @@ -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") diff --git a/crates/aim-core/tests/install_scope.rs b/crates/upm-core/tests/install_scope.rs similarity index 63% rename from crates/aim-core/tests/install_scope.rs rename to crates/upm-core/tests/install_scope.rs index c0411ce..b868999 100644 --- a/crates/aim-core/tests/install_scope.rs +++ b/crates/upm-core/tests/install_scope.rs @@ -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() { diff --git a/crates/aim-core/tests/metadata_contract.rs b/crates/upm-core/tests/metadata_contract.rs similarity index 73% rename from crates/aim-core/tests/metadata_contract.rs rename to crates/upm-core/tests/metadata_contract.rs index 24d0ba2..50bad24 100644 --- a/crates/aim-core/tests/metadata_contract.rs +++ b/crates/upm-core/tests/metadata_contract.rs @@ -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() { diff --git a/crates/aim-core/tests/metadata_electron_builder.rs b/crates/upm-core/tests/metadata_electron_builder.rs similarity index 80% rename from crates/aim-core/tests/metadata_electron_builder.rs rename to crates/upm-core/tests/metadata_electron_builder.rs index cb9a474..d88e09f 100644 --- a/crates/aim-core/tests/metadata_electron_builder.rs +++ b/crates/upm-core/tests/metadata_electron_builder.rs @@ -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() { diff --git a/crates/aim-core/tests/metadata_zsync.rs b/crates/upm-core/tests/metadata_zsync.rs similarity index 76% rename from crates/aim-core/tests/metadata_zsync.rs rename to crates/upm-core/tests/metadata_zsync.rs index c13fd4c..d7079e2 100644 --- a/crates/aim-core/tests/metadata_zsync.rs +++ b/crates/upm-core/tests/metadata_zsync.rs @@ -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() { diff --git a/crates/aim-core/tests/platform_detection.rs b/crates/upm-core/tests/platform_detection.rs similarity index 93% rename from crates/aim-core/tests/platform_detection.rs rename to crates/upm-core/tests/platform_detection.rs index 424eeab..726f852 100644 --- a/crates/aim-core/tests/platform_detection.rs +++ b/crates/upm-core/tests/platform_detection.rs @@ -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() { diff --git a/crates/upm-core/tests/provider_registry.rs b/crates/upm-core/tests/provider_registry.rs new file mode 100644 index 0000000..f4f2a78 --- /dev/null +++ b/crates/upm-core/tests/provider_registry.rs @@ -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, 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, 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, + ®istry, + 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, + ®istry, + 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") + ); +} diff --git a/crates/aim-core/tests/query_resolution.rs b/crates/upm-core/tests/query_resolution.rs similarity index 94% rename from crates/aim-core/tests/query_resolution.rs rename to crates/upm-core/tests/query_resolution.rs index 4a24583..8578ebd 100644 --- a/crates/aim-core/tests/query_resolution.rs +++ b/crates/upm-core/tests/query_resolution.rs @@ -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] diff --git a/crates/aim-core/tests/registry_roundtrip.rs b/crates/upm-core/tests/registry_roundtrip.rs similarity index 78% rename from crates/aim-core/tests/registry_roundtrip.rs rename to crates/upm-core/tests/registry_roundtrip.rs index f0d5ec5..11703b6 100644 --- a/crates/aim-core/tests/registry_roundtrip.rs +++ b/crates/upm-core/tests/registry_roundtrip.rs @@ -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, diff --git a/crates/aim-core/tests/remove_flow.rs b/crates/upm-core/tests/remove_flow.rs similarity index 86% rename from crates/aim-core/tests/remove_flow.rs rename to crates/upm-core/tests/remove_flow.rs index b15927a..fea6183 100644 --- a/crates/aim-core/tests/remove_flow.rs +++ b/crates/upm-core/tests/remove_flow.rs @@ -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(), ), diff --git a/crates/aim-core/tests/search_github.rs b/crates/upm-core/tests/search_github.rs similarity index 95% rename from crates/aim-core/tests/search_github.rs rename to crates/upm-core/tests/search_github.rs index 304c6f7..e99f529 100644 --- a/crates/aim-core/tests/search_github.rs +++ b/crates/upm-core/tests/search_github.rs @@ -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, SearchProviderError> { + ) -> Result, SearchProviderError> { Err(SearchProviderError::new("github", "fixture rate limit")) } } diff --git a/crates/aim-core/tests/show_resolution.rs b/crates/upm-core/tests/show_resolution.rs similarity index 95% rename from crates/aim-core/tests/show_resolution.rs rename to crates/upm-core/tests/show_resolution.rs index d7bd78f..99853f2 100644 --- a/crates/aim-core/tests/show_resolution.rs +++ b/crates/upm-core/tests/show_resolution.rs @@ -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:?}"), diff --git a/crates/aim-core/tests/update_planning.rs b/crates/upm-core/tests/update_planning.rs similarity index 95% rename from crates/aim-core/tests/update_planning.rs rename to crates/upm-core/tests/update_planning.rs index d45db1f..f5c391e 100644 --- a/crates/aim-core/tests/update_planning.rs +++ b/crates/upm-core/tests/update_planning.rs @@ -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() ); } diff --git a/crates/aim-cli/Cargo.toml b/crates/upm/Cargo.toml similarity index 81% rename from crates/aim-cli/Cargo.toml rename to crates/upm/Cargo.toml index cfc65ec..f1e495c 100644 --- a/crates/aim-cli/Cargo.toml +++ b/crates/upm/Cargo.toml @@ -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 diff --git a/crates/aim-cli/src/cli/args.rs b/crates/upm/src/cli/args.rs similarity index 89% rename from crates/aim-cli/src/cli/args.rs rename to crates/upm/src/cli/args.rs index 1bdc5a3..4a8a0a8 100644 --- a/crates/aim-cli/src/cli/args.rs +++ b/crates/upm/src/cli/args.rs @@ -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, diff --git a/crates/aim-cli/src/cli/config.rs b/crates/upm/src/cli/config.rs similarity index 97% rename from crates/aim-cli/src/cli/config.rs rename to crates/upm/src/cli/config.rs index 73a40bd..6349c77 100644 --- a/crates/aim-cli/src/cli/config.rs +++ b/crates/upm/src/cli/config.rs @@ -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()) } diff --git a/crates/aim-cli/src/cli/mod.rs b/crates/upm/src/cli/mod.rs similarity index 100% rename from crates/aim-cli/src/cli/mod.rs rename to crates/upm/src/cli/mod.rs diff --git a/crates/aim-cli/src/config.rs b/crates/upm/src/config.rs similarity index 94% rename from crates/aim-cli/src/config.rs rename to crates/upm/src/config.rs index 3397216..fcff83a 100644 --- a/crates/aim-cli/src/config.rs +++ b/crates/upm/src/config.rs @@ -68,16 +68,16 @@ pub fn load_from_path(path: &Path) -> Result { } 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)] diff --git a/crates/aim-cli/src/lib.rs b/crates/upm/src/lib.rs similarity index 69% rename from crates/aim-cli/src/lib.rs rename to crates/upm/src/lib.rs index a9e5e6a..ba99cb5 100644 --- a/crates/aim-cli/src/lib.rs +++ b/crates/upm/src/lib.rs @@ -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 for DispatchError { - fn from(value: aim_core::app::add::BuildAddPlanError) -> Self { +impl From for DispatchError { + fn from(value: upm_core::app::add::BuildAddPlanError) -> Self { Self::AddPlan(value) } } -impl From for DispatchError { - fn from(value: aim_core::app::add::InstallAppError) -> Self { +impl From for DispatchError { + fn from(value: upm_core::app::add::InstallAppError) -> Self { Self::AddInstall(value) } } @@ -366,38 +387,38 @@ impl From for DispatchError { } } -impl From for DispatchError { - fn from(value: aim_core::app::update::BuildUpdatePlanError) -> Self { +impl From for DispatchError { + fn from(value: upm_core::app::update::BuildUpdatePlanError) -> Self { Self::UpdatePlan(value) } } -impl From for DispatchError { - fn from(value: aim_core::app::update::ExecuteUpdatesError) -> Self { +impl From for DispatchError { + fn from(value: upm_core::app::update::ExecuteUpdatesError) -> Self { Self::UpdateExecution(value) } } -impl From for DispatchError { - fn from(value: aim_core::app::remove::RemoveRegisteredAppError) -> Self { +impl From for DispatchError { + fn from(value: upm_core::app::remove::RemoveRegisteredAppError) -> Self { Self::RemovePlan(value) } } -impl From for DispatchError { - fn from(value: aim_core::registry::store::RegistryStoreError) -> Self { +impl From for DispatchError { + fn from(value: upm_core::registry::store::RegistryStoreError) -> Self { Self::Registry(value) } } -impl From for DispatchError { - fn from(value: aim_core::app::search::SearchError) -> Self { +impl From for DispatchError { + fn from(value: upm_core::app::search::SearchError) -> Self { Self::Search(value) } } -impl From for DispatchError { - fn from(value: aim_core::domain::show::ShowResultError) -> Self { +impl From 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"); } diff --git a/crates/aim-cli/src/main.rs b/crates/upm/src/main.rs similarity index 53% rename from crates/aim-cli/src/main.rs rename to crates/upm/src/main.rs index eea6ffa..cbb5f3e 100644 --- a/crates/aim-cli/src/main.rs +++ b/crates/upm/src/main.rs @@ -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!(); diff --git a/crates/upm/src/providers.rs b/crates/upm/src/providers.rs new file mode 100644 index 0000000..4bcc721 --- /dev/null +++ b/crates/upm/src/providers.rs @@ -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(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) +} diff --git a/crates/aim-cli/src/ui/mod.rs b/crates/upm/src/ui/mod.rs similarity index 100% rename from crates/aim-cli/src/ui/mod.rs rename to crates/upm/src/ui/mod.rs diff --git a/crates/aim-cli/src/ui/progress.rs b/crates/upm/src/ui/progress.rs similarity index 98% rename from crates/aim-cli/src/ui/progress.rs rename to crates/upm/src/ui/progress.rs index 6a42ea9..29e7489 100644 --- a/crates/aim-cli/src/ui/progress.rs +++ b/crates/upm/src/ui/progress.rs @@ -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) -> 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() { diff --git a/crates/aim-cli/src/ui/prompt.rs b/crates/upm/src/ui/prompt.rs similarity index 94% rename from crates/aim-cli/src/ui/prompt.rs rename to crates/upm/src/ui/prompt.rs index c3edf41..2bbc4cb 100644 --- a/crates/aim-cli/src/ui/prompt.rs +++ b/crates/upm/src/ui/prompt.rs @@ -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 { diff --git a/crates/aim-cli/src/ui/render.rs b/crates/upm/src/ui/render.rs similarity index 94% rename from crates/aim-cli/src/ui/render.rs rename to crates/upm/src/ui/render.rs index 2581bb7..47a5238 100644 --- a/crates/aim-cli/src/ui/render.rs +++ b/crates/upm/src/ui/render.rs @@ -1,10 +1,10 @@ -use aim_core::app::add::AddPlan; -use aim_core::domain::search::SearchResults; -use aim_core::domain::show::{ +use console::measure_text_width; +use upm_core::app::add::AddPlan; +use upm_core::domain::search::SearchResults; +use upm_core::domain::show::{ InstalledShow, MetadataSummary, RemoteInteractionSummary, RemoteShow, ShowResult, SourceSummary, }; -use aim_core::domain::update::UpdateExecutionStatus; -use console::measure_text_width; +use upm_core::domain::update::UpdateExecutionStatus; use crate::DispatchResult; use crate::config::CliConfig; @@ -38,10 +38,10 @@ pub fn render_dispatch_result_with_config(result: &DispatchResult, config: &CliC } } -fn render_added_app(added: &aim_core::app::add::InstalledApp) -> String { +fn render_added_app(added: &upm_core::app::add::InstalledApp) -> String { let scope = match added.install_scope { - aim_core::domain::app::InstallScope::User => "user", - aim_core::domain::app::InstallScope::System => "system", + upm_core::domain::app::InstallScope::User => "user", + upm_core::domain::app::InstallScope::System => "system", }; let warning_lines = added @@ -104,7 +104,7 @@ fn render_pending_add(plan: &AddPlan) -> String { .join("\n") } -fn render_list(rows: &[aim_core::app::list::ListRow]) -> String { +fn render_list(rows: &[upm_core::app::list::ListRow]) -> String { if rows.is_empty() { return crate::ui::theme::muted("No installed apps yet"); } @@ -170,7 +170,7 @@ fn format_list_row( } } -fn render_removed_app(removed: &aim_core::app::remove::RemovalResult) -> String { +fn render_removed_app(removed: &upm_core::app::remove::RemovalResult) -> String { let warning_lines = removed .warnings .iter() @@ -385,10 +385,10 @@ fn metadata_detail_lines(metadata: &MetadataSummary) -> Vec { lines } -fn installed_files_header(scope: Option) -> String { +fn installed_files_header(scope: Option) -> String { let label = match scope { - Some(aim_core::domain::app::InstallScope::User) => "Installed as User", - Some(aim_core::domain::app::InstallScope::System) => "Installed as System", + Some(upm_core::domain::app::InstallScope::User) => "Installed as User", + Some(upm_core::domain::app::InstallScope::System) => "Installed as System", None => "Installed files", }; @@ -422,11 +422,11 @@ fn truncate_checksum(checksum: &str) -> String { } } -fn metadata_kind_label(kind: aim_core::domain::update::ParsedMetadataKind) -> &'static str { +fn metadata_kind_label(kind: upm_core::domain::update::ParsedMetadataKind) -> &'static str { match kind { - aim_core::domain::update::ParsedMetadataKind::Unknown => "unknown", - aim_core::domain::update::ParsedMetadataKind::ElectronBuilder => "electron-builder", - aim_core::domain::update::ParsedMetadataKind::Zsync => "zsync", + upm_core::domain::update::ParsedMetadataKind::Unknown => "unknown", + upm_core::domain::update::ParsedMetadataKind::ElectronBuilder => "electron-builder", + upm_core::domain::update::ParsedMetadataKind::Zsync => "zsync", } } @@ -509,7 +509,7 @@ fn render_remote_show(remote: &RemoteShow) -> String { lines.join("\n") } -fn install_file_paths(added: &aim_core::app::add::InstalledApp) -> Vec { +fn install_file_paths(added: &upm_core::app::add::InstalledApp) -> Vec { [ Some( added @@ -595,7 +595,7 @@ fn render_search_results_with_config(results: &SearchResults, config: &CliConfig render_search_results(results) } -fn render_updated_apps(result: &aim_core::domain::update::UpdateExecutionResult) -> String { +fn render_updated_apps(result: &upm_core::domain::update::UpdateExecutionResult) -> String { let mut lines = vec![ crate::ui::theme::heading("Update Summary"), format!("updated apps: {}", result.updated_count()), @@ -621,7 +621,7 @@ fn render_updated_apps(result: &aim_core::domain::update::UpdateExecutionResult) lines.join("\n") } -fn render_update_plan(plan: &aim_core::domain::update::UpdatePlan) -> String { +fn render_update_plan(plan: &upm_core::domain::update::UpdatePlan) -> String { let mut lines = vec![render_update_summary(plan.items.len(), plan.items.len(), 0)]; for item in &plan.items { diff --git a/crates/aim-cli/src/ui/search_browser.rs b/crates/upm/src/ui/search_browser.rs similarity index 99% rename from crates/aim-cli/src/ui/search_browser.rs rename to crates/upm/src/ui/search_browser.rs index b518637..8a8616c 100644 --- a/crates/aim-cli/src/ui/search_browser.rs +++ b/crates/upm/src/ui/search_browser.rs @@ -2,7 +2,6 @@ use std::collections::BTreeSet; use std::io::IsTerminal; use std::time::Duration; -use aim_core::domain::search::{SearchInstallStatus, SearchResult, SearchResults}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::execute; use crossterm::terminal::{ @@ -14,6 +13,7 @@ use ratatui::style::Modifier; use ratatui::text::{Line, Span}; use ratatui::widgets::{Clear, List, ListItem, Paragraph, Wrap}; use ratatui::{Frame, Terminal}; +use upm_core::domain::search::{SearchInstallStatus, SearchResult, SearchResults}; use crate::config::{CliConfig, SearchConfig}; diff --git a/crates/aim-cli/src/ui/theme.rs b/crates/upm/src/ui/theme.rs similarity index 100% rename from crates/aim-cli/src/ui/theme.rs rename to crates/upm/src/ui/theme.rs diff --git a/crates/aim-cli/tests/cli_commands.rs b/crates/upm/tests/cli_commands.rs similarity index 78% rename from crates/aim-cli/tests/cli_commands.rs rename to crates/upm/tests/cli_commands.rs index 3842b51..aa6ad28 100644 --- a/crates/aim-cli/tests/cli_commands.rs +++ b/crates/upm/tests/cli_commands.rs @@ -1,15 +1,15 @@ use assert_cmd::Command; use predicates::str::contains; -use aim_cli::cli::args::Command as AimCommand; -use aim_cli::{Cli, DispatchError}; -use aim_core::domain::show::{ShowResultError, SourceSummary}; -use aim_core::domain::source::SourceKind; use clap::Parser; +use upm::cli::args::Command as UpmCommand; +use upm::{Cli, DispatchError}; +use upm_core::domain::show::{ShowResultError, SourceSummary}; +use upm_core::domain::source::SourceKind; #[test] fn help_lists_expected_commands() { - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("--help") .assert() .success() @@ -22,20 +22,20 @@ fn help_lists_expected_commands() { #[test] fn cli_parses_show_subcommand() { - let cli = Cli::try_parse_from(["aim", "show", "legacy-bat"]).unwrap(); + let cli = Cli::try_parse_from(["upm", "show", "legacy-bat"]).unwrap(); match cli.command { - Some(AimCommand::Show { value }) => assert_eq!(value.as_deref(), Some("legacy-bat")), + Some(UpmCommand::Show { value }) => assert_eq!(value.as_deref(), Some("legacy-bat")), other => panic!("expected show command, got {other:?}"), } } #[test] fn cli_parses_bare_show_subcommand() { - let cli = Cli::try_parse_from(["aim", "show"]).unwrap(); + let cli = Cli::try_parse_from(["upm", "show"]).unwrap(); match cli.command { - Some(AimCommand::Show { value }) => assert_eq!(value, None), + Some(UpmCommand::Show { value }) => assert_eq!(value, None), other => panic!("expected bare show command, got {other:?}"), } } diff --git a/crates/aim-cli/tests/cli_smoke.rs b/crates/upm/tests/cli_smoke.rs similarity index 64% rename from crates/aim-cli/tests/cli_smoke.rs rename to crates/upm/tests/cli_smoke.rs index 39d11be..0571d24 100644 --- a/crates/aim-cli/tests/cli_smoke.rs +++ b/crates/upm/tests/cli_smoke.rs @@ -2,6 +2,6 @@ use assert_cmd::Command; #[test] fn cli_shows_help() { - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("--help").assert().success(); } diff --git a/crates/upm/tests/config_loading.rs b/crates/upm/tests/config_loading.rs new file mode 100644 index 0000000..badd37a --- /dev/null +++ b/crates/upm/tests/config_loading.rs @@ -0,0 +1,167 @@ +use std::sync::Mutex; + +use tempfile::tempdir; +use upm::config::{ + CliConfig, ConfigError, SearchConfig, ThemeConfig, default_path, load_from_path, +}; +use upm::default_registry_path; + +static ENV_LOCK: Mutex<()> = Mutex::new(()); + +struct EnvGuard { + key: &'static str, + original: Option, +} + +impl EnvGuard { + fn set(key: &'static str, value: impl AsRef) -> Self { + let original = std::env::var_os(key); + unsafe { + std::env::set_var(key, value); + } + Self { key, original } + } + + fn remove(key: &'static str) -> Self { + let original = std::env::var_os(key); + unsafe { + std::env::remove_var(key); + } + Self { key, original } + } +} + +impl Drop for EnvGuard { + fn drop(&mut self) { + match &self.original { + Some(value) => unsafe { + std::env::set_var(self.key, value); + }, + None => unsafe { + std::env::remove_var(self.key); + }, + } + } +} + +#[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: 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:?}"), + } +} + +#[test] +fn default_config_path_uses_upm_directory() { + let _guard = ENV_LOCK.lock().unwrap(); + let dir = tempdir().unwrap(); + + let _config_path = EnvGuard::remove("UPM_CONFIG_PATH"); + let _xdg_config_home = EnvGuard::remove("XDG_CONFIG_HOME"); + let _home = EnvGuard::set("HOME", dir.path()); + + let path = default_path(); + + assert_eq!(path, dir.path().join(".config/upm/config.toml")); +} + +#[test] +fn default_config_path_ignores_legacy_aim_override() { + let _guard = ENV_LOCK.lock().unwrap(); + let dir = tempdir().unwrap(); + let legacy_path = dir.path().join("aim-config.toml"); + + let _legacy_config_path = EnvGuard::set("AIM_CONFIG_PATH", &legacy_path); + let _config_path = EnvGuard::remove("UPM_CONFIG_PATH"); + let _xdg_config_home = EnvGuard::remove("XDG_CONFIG_HOME"); + let _home = EnvGuard::set("HOME", dir.path()); + + let path = default_path(); + + assert_eq!(path, dir.path().join(".config/upm/config.toml")); +} + +#[test] +fn default_registry_path_uses_upm_directory() { + let _guard = ENV_LOCK.lock().unwrap(); + let dir = tempdir().unwrap(); + + let _registry_path = EnvGuard::remove("UPM_REGISTRY_PATH"); + let _home = EnvGuard::set("HOME", dir.path()); + + let path = default_registry_path(); + + assert_eq!(path, dir.path().join(".local/share/upm/registry.toml")); +} + +#[test] +fn default_registry_path_ignores_legacy_aim_override() { + let _guard = ENV_LOCK.lock().unwrap(); + let dir = tempdir().unwrap(); + let legacy_path = dir.path().join("aim-registry.toml"); + + let _legacy_registry_path = EnvGuard::set("AIM_REGISTRY_PATH", &legacy_path); + let _registry_path = EnvGuard::remove("UPM_REGISTRY_PATH"); + let _home = EnvGuard::set("HOME", dir.path()); + + let path = default_registry_path(); + + assert_eq!(path, dir.path().join(".local/share/upm/registry.toml")); +} diff --git a/crates/aim-cli/tests/end_to_end_cli.rs b/crates/upm/tests/end_to_end_cli.rs similarity index 82% rename from crates/aim-cli/tests/end_to_end_cli.rs rename to crates/upm/tests/end_to_end_cli.rs index 8bdf7ab..2cc7aa3 100644 --- a/crates/aim-cli/tests/end_to_end_cli.rs +++ b/crates/upm/tests/end_to_end_cli.rs @@ -1,21 +1,21 @@ -use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; -use aim_core::registry::model::Registry; -use aim_core::registry::store::RegistryStore; use assert_cmd::Command; use predicates::prelude::PredicateBooleanExt; use predicates::str::contains; use tempfile::tempdir; +use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; +use upm_core::registry::model::Registry; +use upm_core::registry::store::RegistryStore; -const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE"; +const FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE"; #[test] fn list_command_runs_without_registry_entries() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("list") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .assert() .success() .stdout(contains("No installed apps yet")); @@ -31,10 +31,10 @@ fn list_command_reads_registered_apps_from_registry_file() { ) .unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("list") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .assert() .success() .stdout(contains("Name")) @@ -54,10 +54,10 @@ fn remove_command_removes_registered_app_from_registry_file() { ) .unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["remove", "bat"]) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .assert() .success() .stdout(contains("Removed Bat")) @@ -73,14 +73,14 @@ fn remove_command_uninstalls_managed_files() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); let install_home = dir.path().join("install-home"); - let payload_path = install_home.join(".local/lib/aim/appimages/sharkdp-bat.AppImage"); - let desktop_path = install_home.join(".local/share/applications/aim-sharkdp-bat.desktop"); + let payload_path = install_home.join(".local/lib/upm/appimages/sharkdp-bat.AppImage"); + let desktop_path = install_home.join(".local/share/applications/upm-sharkdp-bat.desktop"); let icon_path = install_home.join(".local/share/icons/hicolor/256x256/apps/sharkdp-bat.png"); - let mut add_cmd = Command::cargo_bin("aim").unwrap(); + let mut add_cmd = Command::cargo_bin("upm").unwrap(); add_cmd .arg("sharkdp/bat") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success(); @@ -89,10 +89,10 @@ fn remove_command_uninstalls_managed_files() { assert!(desktop_path.exists()); assert!(icon_path.exists()); - let mut remove_cmd = Command::cargo_bin("aim").unwrap(); + let mut remove_cmd = Command::cargo_bin("upm").unwrap(); remove_cmd .args(["remove", "sharkdp-bat"]) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .assert() .success() .stdout(contains("\nRemoved bat")) @@ -101,7 +101,7 @@ fn remove_command_uninstalls_managed_files() { .stdout(contains("Removed app:").not()) .stdout(contains("Removed files")) .stdout(contains("sharkdp-bat.AppImage")) - .stdout(contains("aim-sharkdp-bat.desktop")) + .stdout(contains("upm-sharkdp-bat.desktop")) .stdout(contains("sharkdp-bat.png")); assert!(!payload_path.exists()); @@ -113,10 +113,10 @@ fn remove_command_uninstalls_managed_files() { fn query_command_registers_unambiguous_app_in_registry_file() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("sharkdp/bat") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -140,10 +140,10 @@ fn query_command_registers_unambiguous_app_in_registry_file() { fn old_release_query_renders_tracking_prompt_without_writing_registry() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -158,12 +158,12 @@ fn old_release_query_renders_tracking_prompt_without_writing_registry() { fn old_release_query_can_track_latest_and_register_app() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") - .env("AIM_TRACKING_PREFERENCE", "latest") + .env("UPM_TRACKING_PREFERENCE", "latest") .assert() .success() .stdout(contains("\nInstalled t3code (user)")) @@ -182,14 +182,34 @@ fn old_release_query_can_track_latest_and_register_app() { assert!(contents.contains("locator = \"pingdotgg/t3code\"")); } +#[test] +fn old_release_query_ignores_legacy_tracking_preference_env() { + let dir = tempdir().unwrap(); + let registry_path = dir.path().join("registry.toml"); + let mut cmd = Command::cargo_bin("upm").unwrap(); + + cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage") + .env("UPM_REGISTRY_PATH", ®istry_path) + .env(FIXTURE_MODE_ENV, "1") + .env("AIM_TRACKING_PREFERENCE", "latest") + .assert() + .success() + .stdout(contains("Choose update tracking")) + .stdout(contains("v0.0.11")) + .stdout(contains("v0.0.12")) + .stdout(contains("Installed t3code").not()); + + assert!(!registry_path.exists()); +} + #[test] fn cli_add_installs_and_renders_resolved_mode() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("sharkdp/bat") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -204,10 +224,10 @@ fn cli_add_installs_and_renders_resolved_mode() { fn positional_query_falls_back_to_search_for_plain_name_queries() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("firefox") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -226,10 +246,10 @@ fn positional_query_falls_back_to_search_for_plain_name_queries() { fn positional_query_falls_back_to_empty_search_when_direct_item_has_no_appimage() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("appimagehub/2337998") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -246,10 +266,10 @@ fn positional_query_falls_back_to_empty_search_when_direct_item_has_no_appimage( fn cli_add_installs_appimagehub_source_with_truthful_origin() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("appimagehub/2338455") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -273,10 +293,10 @@ fn cli_add_installs_appimagehub_source_with_truthful_origin() { fn cli_add_installs_gitlab_source_with_truthful_origin() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("https://gitlab.com/example/team-app") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -298,10 +318,10 @@ fn cli_add_preserves_direct_url_origin_for_provider_like_downloads() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); let query = "https://sourceforge.net/projects/team-app/files/team-app-1.0.0.AppImage/download"; - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg(query) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -321,10 +341,10 @@ fn cli_add_installs_sourceforge_latest_download_with_truthful_origin() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); let query = "https://sourceforge.net/projects/team-app/files/latest/download"; - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg(query) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -343,10 +363,10 @@ fn cli_add_installs_sourceforge_latest_download_with_truthful_origin() { fn cli_rejects_insecure_http_direct_urls_by_default() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("http://example.com/team-app.AppImage") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .assert() .failure() .stderr(contains("insecure HTTP sources are disabled")); @@ -360,11 +380,11 @@ fn cli_allows_insecure_http_direct_urls_when_config_enables_it() { let registry_path = dir.path().join("registry.toml"); let config_path = dir.path().join("config.toml"); std::fs::write(&config_path, "allow_http = true\n").unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("http://example.com/team-app.AppImage") - .env("AIM_REGISTRY_PATH", ®istry_path) - .env("AIM_CONFIG_PATH", &config_path) + .env("UPM_REGISTRY_PATH", ®istry_path) + .env("UPM_CONFIG_PATH", &config_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -380,13 +400,13 @@ fn cli_rejects_insecure_appimagehub_download_urls_even_when_http_is_allowed() { let registry_path = dir.path().join("registry.toml"); let config_path = dir.path().join("config.toml"); std::fs::write(&config_path, "allow_http = true\n").unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("appimagehub/2338455") - .env("AIM_REGISTRY_PATH", ®istry_path) - .env("AIM_CONFIG_PATH", &config_path) + .env("UPM_REGISTRY_PATH", ®istry_path) + .env("UPM_CONFIG_PATH", &config_path) .env(FIXTURE_MODE_ENV, "1") - .env("AIM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP", "1") + .env("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP", "1") .assert() .failure() .stderr(contains("insecure appimagehub download url")); @@ -396,12 +416,12 @@ fn cli_rejects_insecure_appimagehub_download_urls_even_when_http_is_allowed() { fn cli_rejects_appimagehub_install_when_md5_does_not_match() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("appimagehub/2338455") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") - .env("AIM_APPIMAGEHUB_FIXTURE_BAD_MD5", "1") + .env("UPM_APPIMAGEHUB_FIXTURE_BAD_MD5", "1") .assert() .failure() .stderr(contains("weak provider checksum did not match")); @@ -412,10 +432,10 @@ fn cli_add_installs_sourceforge_release_folder_with_truthful_origin() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); let query = "https://sourceforge.net/projects/team-app/files/releases/beta/download"; - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg(query) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -436,10 +456,10 @@ fn cli_add_file_like_sourceforge_release_download_stores_releases_root_and_prese let registry_path = dir.path().join("registry.toml"); let query = "https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download"; - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg(query) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -462,10 +482,10 @@ fn cli_add_file_like_sourceforge_release_download_stores_releases_root_and_prese fn cli_reports_unsupported_source_queries_distinctly() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("https://gitlab.com/example") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -479,10 +499,10 @@ fn cli_reports_unsupported_source_queries_distinctly() { fn cli_reports_supported_sources_without_installable_artifacts_distinctly() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("https://sourceforge.net/projects/team-app/") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -496,10 +516,10 @@ fn cli_reports_supported_sources_without_installable_artifacts_distinctly() { fn cli_add_emits_live_progress_to_stderr() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("sharkdp/bat") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -517,7 +537,7 @@ fn cli_add_emits_live_progress_to_stderr() { } #[test] -fn bare_aim_review_renders_review_heading() { +fn bare_upm_review_renders_review_heading() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); let store = RegistryStore::new(registry_path.clone()); @@ -542,9 +562,9 @@ fn bare_aim_review_renders_review_heading() { }) .unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); - cmd.env("AIM_REGISTRY_PATH", ®istry_path) + cmd.env("UPM_REGISTRY_PATH", ®istry_path) .assert() .success() .stdout(contains("Update Review")) @@ -561,10 +581,10 @@ fn remove_command_emits_live_progress_to_stderr() { ) .unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["remove", "bat"]) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .assert() .success() .stderr(contains("Removing bat")) @@ -579,11 +599,11 @@ fn system_request_on_immutable_host_falls_back_to_user_install() { let os_release_path = dir.path().join("os-release"); std::fs::write(&os_release_path, "ID=fedora\nVARIANT_ID=silverblue\n").unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["--system", "sharkdp/bat"]) - .env("AIM_REGISTRY_PATH", ®istry_path) - .env("AIM_OS_RELEASE_PATH", &os_release_path) + .env("UPM_REGISTRY_PATH", ®istry_path) + .env("UPM_OS_RELEASE_PATH", &os_release_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -597,7 +617,7 @@ fn update_command_applies_updates() { let registry_path = dir.path().join("registry.toml"); let payload_path = dir .path() - .join("install-home/.local/lib/aim/appimages/pingdotgg-t3code.AppImage"); + .join("install-home/.local/lib/upm/appimages/pingdotgg-t3code.AppImage"); let store = RegistryStore::new(registry_path.clone()); store .save(&Registry { @@ -620,10 +640,10 @@ fn update_command_applies_updates() { }) .unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("update") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -663,10 +683,10 @@ fn update_command_emits_live_progress_to_stderr() { }) .unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("update") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -682,7 +702,7 @@ fn update_command_reports_when_previous_installation_is_restored() { let install_home = dir.path().join("install-home"); let store = RegistryStore::new(registry_path.clone()); let stable_id = "url-example.com-downloads-team-app.appimage"; - let payload_path = install_home.join(format!(".local/lib/aim/appimages/{stable_id}.AppImage")); + let payload_path = install_home.join(format!(".local/lib/upm/appimages/{stable_id}.AppImage")); std::fs::create_dir_all(payload_path.parent().unwrap()).unwrap(); std::fs::write(&payload_path, b"previous-payload").unwrap(); @@ -696,11 +716,11 @@ fn update_command_reports_when_previous_installation_is_restored() { stable_id: stable_id.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, @@ -719,10 +739,10 @@ fn update_command_reports_when_previous_installation_is_restored() { }) .unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.arg("update") - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .env("DISPLAY", ":99") .env("XDG_CURRENT_DESKTOP", "test") diff --git a/crates/aim-cli/tests/search_browser.rs b/crates/upm/tests/search_browser.rs similarity index 93% rename from crates/aim-cli/tests/search_browser.rs rename to crates/upm/tests/search_browser.rs index b355a3d..1a88b87 100644 --- a/crates/aim-cli/tests/search_browser.rs +++ b/crates/upm/tests/search_browser.rs @@ -1,6 +1,6 @@ -use aim_cli::config::SearchConfig; -use aim_cli::ui::search_browser::{BrowserPhase, SearchBrowserState, SubmitAction}; -use aim_core::domain::search::{SearchInstallStatus, SearchResult}; +use upm::config::SearchConfig; +use upm::ui::search_browser::{BrowserPhase, SearchBrowserState, SubmitAction}; +use upm_core::domain::search::{SearchInstallStatus, SearchResult}; #[test] fn browser_defaults_to_bottom_to_top_ordering() { @@ -81,7 +81,7 @@ fn invalid_numeric_input_keeps_last_good_selection_visible() { #[test] fn highlight_segments_marks_matching_query_fragments() { - let fragments = aim_cli::ui::search_browser::highlight_segments("pingdotgg/t3code", "dotgg"); + let fragments = upm::ui::search_browser::highlight_segments("pingdotgg/t3code", "dotgg"); assert_eq!(fragments.len(), 3); assert_eq!(fragments[1].text, "dotgg"); @@ -123,8 +123,8 @@ fn submit_selection_can_skip_confirmation_from_config() { assert_eq!( action, - SubmitAction::Confirmed(aim_cli::ui::search_browser::SearchSelection { - rows: vec![aim_cli::ui::search_browser::SearchRow { + SubmitAction::Confirmed(upm::ui::search_browser::SearchSelection { + rows: vec![upm::ui::search_browser::SearchRow { status: SearchInstallStatus::Available, provider_id: "github".to_owned(), display_name: "charlie/app".to_owned(), diff --git a/crates/aim-cli/tests/search_cli.rs b/crates/upm/tests/search_cli.rs similarity index 82% rename from crates/aim-cli/tests/search_cli.rs rename to crates/upm/tests/search_cli.rs index 8f0a1d2..5b0d977 100644 --- a/crates/aim-cli/tests/search_cli.rs +++ b/crates/upm/tests/search_cli.rs @@ -3,16 +3,16 @@ use predicates::prelude::PredicateBooleanExt; use predicates::str::contains; use tempfile::tempdir; -const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE"; +const FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE"; #[test] fn search_command_renders_remote_github_results() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["search", "bat"]) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -46,10 +46,10 @@ fn search_command_renders_local_matches_in_deterministic_order() { ) .unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["search", "bat"]) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -69,10 +69,10 @@ fn search_command_is_read_only_for_registry_contents() { let original = "version = 1\n[[apps]]\nstable_id = \"bat\"\ndisplay_name = \"Bat\"\n"; std::fs::write(®istry_path, original).unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["search", "bat"]) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success(); @@ -88,11 +88,11 @@ fn search_command_fails_fast_on_malformed_config() { let config_path = dir.path().join("config.toml"); std::fs::write(&config_path, "[search\nskip_confirmation = true\n").unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["search", "bat"]) - .env("AIM_REGISTRY_PATH", ®istry_path) - .env("AIM_CONFIG_PATH", &config_path) + .env("UPM_REGISTRY_PATH", ®istry_path) + .env("UPM_CONFIG_PATH", &config_path) .env(FIXTURE_MODE_ENV, "1") .assert() .failure() @@ -110,11 +110,11 @@ fn search_command_uses_plain_text_output_when_not_on_a_tty() { ) .unwrap(); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["search", "bat"]) - .env("AIM_REGISTRY_PATH", ®istry_path) - .env("AIM_CONFIG_PATH", &config_path) + .env("UPM_REGISTRY_PATH", ®istry_path) + .env("UPM_CONFIG_PATH", &config_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -127,10 +127,10 @@ fn search_command_uses_plain_text_output_when_not_on_a_tty() { fn search_command_reports_loading_status_to_stderr() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["search", "bat"]) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -141,10 +141,10 @@ fn search_command_reports_loading_status_to_stderr() { fn search_command_keeps_empty_results_in_plain_text_mode() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["search", "no-such-app-image-query"]) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -156,10 +156,10 @@ fn search_command_keeps_empty_results_in_plain_text_mode() { fn search_command_renders_appimagehub_results() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("aim").unwrap(); + let mut cmd = Command::cargo_bin("upm").unwrap(); cmd.args(["search", "firefox"]) - .env("AIM_REGISTRY_PATH", ®istry_path) + .env("UPM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() diff --git a/crates/aim-cli/tests/ui_summary.rs b/crates/upm/tests/ui_summary.rs similarity index 88% rename from crates/aim-cli/tests/ui_summary.rs rename to crates/upm/tests/ui_summary.rs index f7d1d3d..e51ff21 100644 --- a/crates/aim-cli/tests/ui_summary.rs +++ b/crates/upm/tests/ui_summary.rs @@ -1,28 +1,28 @@ -use aim_cli::DispatchResult; -use aim_cli::ui::prompt::render_interaction; -use aim_cli::ui::render::{render_dispatch_result, render_update_summary}; -use aim_cli::ui::search_browser::{SearchRow, format_search_row, render_confirmation_summary}; -use aim_core::app::add::InstalledApp; -use aim_core::app::interaction::{InteractionKind, InteractionRequest}; -use aim_core::app::list::ListRow; -use aim_core::app::remove::{RemovalPlan, RemovalResult}; -use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; -use aim_core::domain::search::SearchInstallStatus; -use aim_core::domain::show::{ +use upm::DispatchResult; +use upm::ui::prompt::render_interaction; +use upm::ui::render::{render_dispatch_result, render_update_summary}; +use upm::ui::search_browser::{SearchRow, format_search_row, render_confirmation_summary}; +use upm_core::app::add::InstalledApp; +use upm_core::app::interaction::{InteractionKind, InteractionRequest}; +use upm_core::app::list::ListRow; +use upm_core::app::remove::{RemovalPlan, RemovalResult}; +use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; +use upm_core::domain::search::SearchInstallStatus; +use upm_core::domain::show::{ InstalledShow, MetadataSummary, RemoteArtifactSummary, RemoteShow, ShowResult, SourceSummary, TrackedInstallPaths, UpdateChannelSummary, UpdateStrategySummary, }; -use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; -use aim_core::domain::update::ArtifactCandidate; -use aim_core::domain::update::{ +use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; +use upm_core::domain::update::ArtifactCandidate; +use upm_core::domain::update::{ ChannelPreference, ParsedMetadataKind, PlannedUpdate, UpdateChannelKind, UpdatePlan, }; -use aim_core::integration::install::InstallOutcome; +use upm_core::integration::install::InstallOutcome; fn muted_bold_label(title: &str) -> String { - let mut style = aim_cli::ui::theme::current_theme().muted; + let mut style = upm::ui::theme::current_theme().muted; style.bold = true; - aim_cli::ui::theme::apply_style_spec(&format!("{title}:"), &style) + upm::ui::theme::apply_style_spec(&format!("{title}:"), &style) } #[test] @@ -87,13 +87,13 @@ fn removal_summary_lists_removed_files() { stable_id: "bat".to_owned(), display_name: "Bat".to_owned(), artifact_paths: vec![ - "/tmp/install-home/.local/lib/aim/appimages/bat.AppImage".to_owned(), - "/tmp/install-home/.local/share/applications/aim-bat.desktop".to_owned(), + "/tmp/install-home/.local/lib/upm/appimages/bat.AppImage".to_owned(), + "/tmp/install-home/.local/share/applications/upm-bat.desktop".to_owned(), ], }, removed_paths: vec![ - "/tmp/install-home/.local/lib/aim/appimages/bat.AppImage".to_owned(), - "/tmp/install-home/.local/share/applications/aim-bat.desktop".to_owned(), + "/tmp/install-home/.local/lib/upm/appimages/bat.AppImage".to_owned(), + "/tmp/install-home/.local/share/applications/upm-bat.desktop".to_owned(), ], remaining_apps: Vec::new(), warnings: Vec::new(), @@ -101,7 +101,7 @@ fn removal_summary_lists_removed_files() { assert!(output.contains("Removed files")); assert!(output.contains("bat.AppImage")); - assert!(output.contains("aim-bat.desktop")); + assert!(output.contains("upm-bat.desktop")); } #[test] @@ -146,10 +146,10 @@ fn install_summary_omits_completed_steps_recap() { install: Some(InstallMetadata { scope: InstallScope::User, payload_path: Some( - "/tmp/install-home/.local/lib/aim/appimages/sharkdp-bat.AppImage".to_owned(), + "/tmp/install-home/.local/lib/upm/appimages/sharkdp-bat.AppImage".to_owned(), ), desktop_entry_path: Some( - "/tmp/install-home/.local/share/applications/aim-sharkdp-bat.desktop" + "/tmp/install-home/.local/share/applications/upm-sharkdp-bat.desktop" .to_owned(), ), icon_path: None, @@ -176,12 +176,12 @@ fn install_summary_omits_completed_steps_recap() { tracks_latest: true, }, install_scope: InstallScope::User, - integration_mode: aim_core::integration::policy::IntegrationMode::Full, + integration_mode: upm_core::integration::policy::IntegrationMode::Full, install_outcome: InstallOutcome { - final_payload_path: "/tmp/install-home/.local/lib/aim/appimages/sharkdp-bat.AppImage" + final_payload_path: "/tmp/install-home/.local/lib/upm/appimages/sharkdp-bat.AppImage" .into(), desktop_entry_path: Some( - "/tmp/install-home/.local/share/applications/aim-sharkdp-bat.desktop".into(), + "/tmp/install-home/.local/share/applications/upm-sharkdp-bat.desktop".into(), ), icon_path: None, warnings: Vec::new(), @@ -285,8 +285,8 @@ fn installed_show_summary_renders_source_scope_and_paths() { install_scope: Some(InstallScope::User), tracked_paths: TrackedInstallPaths { 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()), }, update_strategy: Some(UpdateStrategySummary { preferred: UpdateChannelSummary { @@ -325,30 +325,30 @@ fn installed_show_summary_renders_source_scope_and_paths() { assert!(output.contains(&format!( "{} {}", muted_bold_label("Source"), - aim_cli::ui::theme::muted("github - sharkdp/bat") + upm::ui::theme::muted("github - sharkdp/bat") ))); assert!(output.contains(&format!( "{} {}", muted_bold_label("Update Mechanism"), - aim_cli::ui::theme::muted("electron-builder") + upm::ui::theme::muted("electron-builder") ))); assert!(output.contains(&format!( "{} {}", muted_bold_label("Architecture"), - aim_cli::ui::theme::muted("x86_64") + upm::ui::theme::muted("x86_64") ))); assert!(output.contains(&format!( "{} {}", muted_bold_label("Checksum"), - aim_cli::ui::theme::muted("sha256:abcdefg...456789") + upm::ui::theme::muted("sha256:abcdefg...456789") ))); assert!(output.contains(&muted_bold_label("Installed as User"))); assert!(output.contains("/tmp/bat.AppImage")); - assert!(output.contains("/tmp/aim-bat.desktop")); + assert!(output.contains("/tmp/upm-bat.desktop")); assert!(!output.contains("[up to date] User")); assert!(!output.contains("past version")); - assert!(!output.contains(&aim_cli::ui::theme::label("Metadata"))); - assert!(!output.contains(&aim_cli::ui::theme::label("Files"))); + assert!(!output.contains(&upm::ui::theme::label("Metadata"))); + assert!(!output.contains(&upm::ui::theme::label("Files"))); assert!(!output.contains("abcdefghijklmnopqrstuvwxyz0123456789")); } @@ -427,24 +427,24 @@ fn installed_show_summary_reports_when_newer_versions_are_available() { assert!(output.contains(&format!( "{} {}", muted_bold_label("Source"), - aim_cli::ui::theme::muted("github - pingdotgg/t3code") + upm::ui::theme::muted("github - pingdotgg/t3code") ))); assert!(output.contains(&format!( "{} {}", muted_bold_label("Update Mechanism"), - aim_cli::ui::theme::muted("electron-builder") + upm::ui::theme::muted("electron-builder") ))); assert!(output.contains(&format!( "{} {}", muted_bold_label("Architecture"), - aim_cli::ui::theme::muted("x86_64") + upm::ui::theme::muted("x86_64") ))); assert!(output.contains(&muted_bold_label("Installed as User"))); assert!(!output.contains("[update available] User")); assert!(!output.contains("past versions")); assert!(!output.contains("latest v0.0.16")); - assert!(!output.contains(&aim_cli::ui::theme::label("Metadata"))); - assert!(!output.contains(&aim_cli::ui::theme::label("Files"))); + assert!(!output.contains(&upm::ui::theme::label("Metadata"))); + assert!(!output.contains(&upm::ui::theme::label("Files"))); } #[test] @@ -463,7 +463,7 @@ fn installed_show_list_renders_each_app_using_singular_show_format() { install_scope: Some(InstallScope::User), tracked_paths: TrackedInstallPaths { payload_path: Some("/tmp/bat.AppImage".to_owned()), - desktop_entry_path: Some("/tmp/aim-bat.desktop".to_owned()), + desktop_entry_path: Some("/tmp/upm-bat.desktop".to_owned()), icon_path: None, }, update_strategy: None, @@ -512,12 +512,12 @@ fn installed_show_list_renders_each_app_using_singular_show_format() { assert!(output.contains(&format!( "{} {}", muted_bold_label("Source"), - aim_cli::ui::theme::muted("github - sharkdp/bat") + upm::ui::theme::muted("github - sharkdp/bat") ))); assert!(output.contains(&format!( "{} {}", muted_bold_label("Source"), - aim_cli::ui::theme::muted("github - pingdotgg/t3code") + upm::ui::theme::muted("github - pingdotgg/t3code") ))); }