refactor: add upm application facade and module api
This commit is contained in:
parent
005d6ebfdb
commit
e2a01d3095
36 changed files with 1058 additions and 607 deletions
|
|
@ -2,27 +2,92 @@
|
||||||
|
|
||||||
## Workspace Shape
|
## Workspace Shape
|
||||||
|
|
||||||
`upm` is a Rust workspace with three main crates:
|
`upm` is a Rust workspace with three main crates today and a fourth planned frontend:
|
||||||
|
|
||||||
- `crates/upm-core`: source normalization, add/update orchestration, registry persistence, install policies, desktop integration, and the provider-composition seam.
|
- `crates/upm-core`: the application layer for `upm`. It owns command orchestration, module contracts, module registry and composition, registry persistence, install policies, desktop integration, and the unified frontend-facing API that both the CLI and a future GUI will call.
|
||||||
- `crates/upm`: argument parsing, config loading, terminal UX, prompting, progress reporting, summary rendering, and provider assembly.
|
- `crates/upm`: the CLI frontend over `upm-core`. It handles argument parsing, config loading, terminal UX, prompting, progress reporting, summary rendering, and config-driven module presentation.
|
||||||
- `crates/upm-appimage`: AppImageHub transport, search-provider behavior, and exact add-resolution for AppImage-backed installs.
|
- `crates/upm-appimage`: the AppImage package-manager module. It should own AppImage-specific acquisition backends, artifact selection, and install-resolution behavior.
|
||||||
|
- `crates/upm-ui` (planned): a GUI frontend over `upm-core`, not a second application layer.
|
||||||
|
|
||||||
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.
|
The intended split is strict:
|
||||||
|
|
||||||
|
- `upm-core` is effectively the application
|
||||||
|
- `upm` is one frontend over that application
|
||||||
|
- `upm-ui` will be another frontend over that application
|
||||||
|
- package-manager modules own their own implementation detail and speak to `upm-core` through normalized traits
|
||||||
|
|
||||||
|
That keeps frontend-agnostic logic in `upm-core`, makes a future GUI a first-class consumer instead of a later retrofit, and prevents frontend layers from accumulating package-manager-specific behavior.
|
||||||
|
|
||||||
|
## Application Boundary
|
||||||
|
|
||||||
|
The architectural boundary is:
|
||||||
|
|
||||||
|
- `upm` may know which modules exist for configuration, enablement, disablement, priority, and display
|
||||||
|
- `upm-ui` should operate under the same rule as the CLI: it talks to `upm-core`, not directly to modules
|
||||||
|
- `upm` must not talk directly to a package-manager module or implement module-specific logic
|
||||||
|
- `upm-core` owns the unified application interface used by the CLI now and a GUI later
|
||||||
|
- `upm-core` owns module registration, composition, enablement checks, and request fan-out
|
||||||
|
- `upm-core` fans requests out to enabled modules and aggregates normalized results
|
||||||
|
- each module owns its own internal backends, source quirks, artifact selection, and provider-specific rules
|
||||||
|
|
||||||
|
In practical terms, `upm-core` is where the product behavior lives. The CLI should remain replaceable.
|
||||||
|
|
||||||
|
## Public API Shape
|
||||||
|
|
||||||
|
`upm-core` should expose one high-level application facade to frontend crates.
|
||||||
|
|
||||||
|
- the public boundary should be an application-facing type such as `UpmApp`
|
||||||
|
- the facade should present operations like search, add, show, update, remove, and config management in product terms
|
||||||
|
- frontends should not compose lower-level orchestration services themselves
|
||||||
|
|
||||||
|
That public facade should stay thin. The internal implementation in `upm-core` can and should be split into smaller services such as:
|
||||||
|
|
||||||
|
- module registry and module loading
|
||||||
|
- search orchestration
|
||||||
|
- add planning and execution
|
||||||
|
- show resolution
|
||||||
|
- update planning and execution
|
||||||
|
- configuration and state services
|
||||||
|
|
||||||
|
This gives both frontends one stable application boundary without turning the facade into a god object. The orchestration depth stays inside `upm-core`, where it belongs.
|
||||||
|
|
||||||
|
## Module Tree
|
||||||
|
|
||||||
|
The intended tree is:
|
||||||
|
|
||||||
|
- `upm-core`
|
||||||
|
- public application facade
|
||||||
|
- internal orchestration services
|
||||||
|
- module registry and composition
|
||||||
|
- normalized contracts for package-manager modules
|
||||||
|
- frontend crates
|
||||||
|
- `upm` for CLI concerns only
|
||||||
|
- `upm-ui` for GUI concerns only
|
||||||
|
- module crates
|
||||||
|
- `upm-appimage`
|
||||||
|
- AppImageHub backend
|
||||||
|
- GitHub-backed AppImage acquisition
|
||||||
|
- GitLab-backed AppImage acquisition
|
||||||
|
- SourceForge-backed AppImage acquisition
|
||||||
|
- direct AppImage URL handling
|
||||||
|
- AppImage-specific artifact and metadata rules
|
||||||
|
|
||||||
|
The important constraint is that the top layer understands package-manager modules, not the inner mechanics of how each module finds or resolves artifacts.
|
||||||
|
|
||||||
## Core Flow
|
## Core Flow
|
||||||
|
|
||||||
The main execution path is:
|
The main execution path is:
|
||||||
|
|
||||||
1. Parse CLI input and load runtime config in `upm`.
|
1. Parse CLI input and load runtime config in `upm`.
|
||||||
2. Assemble a `ProviderRegistry` in `crates/upm/src/providers.rs`.
|
2. Call the unified application facade in `upm-core`.
|
||||||
3. Resolve the query into a normalized source in `upm-core`.
|
3. Let `upm-core` route the request into internal orchestration services.
|
||||||
4. Build an add or update plan through core orchestration plus any registered external providers.
|
4. Let those services select enabled modules and fan the request out through normalized module traits.
|
||||||
4. Download the selected AppImage into a staged path.
|
5. Aggregate normalized results into an add, show, update, search, or remove flow.
|
||||||
5. Verify integrity metadata when available.
|
6. Download the selected AppImage into a staged path when the chosen module requires it.
|
||||||
6. Commit the payload into the managed install location.
|
7. Verify integrity metadata when available.
|
||||||
7. Write desktop integration artifacts and refresh helper caches.
|
8. Commit the payload into the managed install location.
|
||||||
8. Persist registry state atomically.
|
9. Write desktop integration artifacts and refresh helper caches.
|
||||||
|
10. Persist registry state atomically.
|
||||||
|
|
||||||
## Source And Provider Model
|
## Source And Provider Model
|
||||||
|
|
||||||
|
|
@ -35,7 +100,9 @@ Supported source classes currently include:
|
||||||
- direct URLs
|
- direct URLs
|
||||||
- local file imports
|
- local file imports
|
||||||
|
|
||||||
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.
|
Core orchestration and normalized module contracts live in `crates/upm-core`. Package-manager-specific behavior belongs in module crates.
|
||||||
|
|
||||||
|
For the AppImage module, that means `crates/upm-appimage` is the package-manager boundary and should grow to own AppImage-specific backing sources internally. `upm-core` should coordinate the module through normalized traits, not absorb AppImageHub, GitHub-backed AppImage discovery, GitLab-backed AppImage discovery, SourceForge-backed AppImage discovery, or direct AppImage URL handling as first-class application concepts.
|
||||||
|
|
||||||
## Runtime Interface
|
## Runtime Interface
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
## Direction
|
## 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 project is evolving from a focused AppImage manager into `upm`, a modular universal package manager. The target system manages multiple package-manager modules through a shared headless application core, keeps frontend crates 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 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 beginning its transition into `upm-appimage` instead of remaining 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.
|
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.
|
||||||
|
|
||||||
|
|
@ -14,7 +14,8 @@ The near-term goal is a Linux-first platform with honest cross-platform architec
|
||||||
- `aim-core` stops being the all-in-one backend and is split.
|
- `aim-core` stops being the all-in-one backend and is split.
|
||||||
- AppImage support becomes an installable module named `upm-appimage`.
|
- AppImage support becomes an installable module named `upm-appimage`.
|
||||||
- Shared orchestration, config, state, resolution, ranking, and frontend-facing APIs move into `upm-core`.
|
- 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 `upm` crate becomes a thin CLI frontend over `upm-core`.
|
||||||
|
- A future `upm-ui` crate will become a GUI frontend over the same `upm-core` application boundary.
|
||||||
- The rename is a hard cutover. Legacy `AIM_*` runtime interfaces are removed rather than preserved.
|
- 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.
|
- 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.
|
- Every `upm` invocation should detect drift between declared state and observed system state, then auto-sync metadata as needed.
|
||||||
|
|
@ -29,9 +30,10 @@ The near-term goal is a Linux-first platform with honest cross-platform architec
|
||||||
The intended workspace shape after the initial refactor is:
|
The intended workspace shape after the initial refactor is:
|
||||||
|
|
||||||
- `upm`: thin CLI frontend, ratatui config UI, command routing, presentation.
|
- `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-ui`: future GUI frontend over the same application core.
|
||||||
- `upm-appimage`: AppImage provider module extracted from the current `aim-core` implementation.
|
- `upm-core`: headless application layer, public application facade, internal orchestration services, module registry, state model, declarative sync engine, ranking, policies, and frontend-agnostic APIs.
|
||||||
- future provider modules: `upm-pacman`, `upm-aur`, `upm-flatpak`, `upm-cargo`, `upm-npm`, and later macOS or Windows-specific modules.
|
- `upm-appimage`: AppImage package-manager module extracted from the current `aim-core` implementation.
|
||||||
|
- future module crates: `upm-pacman`, `upm-aur`, `upm-flatpak`, `upm-cargo`, `upm-npm`, and later macOS or Windows-specific modules.
|
||||||
|
|
||||||
### Module Model
|
### Module Model
|
||||||
|
|
||||||
|
|
@ -42,7 +44,9 @@ UPM should stay modular in both code and packaging:
|
||||||
- distro packaging can offer grouped installs such as `upm-full`
|
- distro packaging can offer grouped installs such as `upm-full`
|
||||||
- lighter installs can ship only the core and selected modules
|
- 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.
|
This means module capabilities, discovery, search, install, remove, inspect, and sync behavior need stable interfaces in `upm-core` rather than package-manager-specific branching in the CLI or GUI.
|
||||||
|
|
||||||
|
The public API shape should be one application facade in `upm-core`, backed by smaller internal services. Frontends should call the facade; they should not compose lower-level services or talk directly to modules.
|
||||||
|
|
||||||
### State Model
|
### State Model
|
||||||
|
|
||||||
|
|
@ -65,7 +69,7 @@ Goals:
|
||||||
- create `upm-core` by extracting reusable infrastructure from `aim-core`
|
- create `upm-core` by extracting reusable infrastructure from `aim-core`
|
||||||
- reduce the CLI crate to a frontend over headless APIs
|
- reduce the CLI crate to a frontend over headless APIs
|
||||||
- isolate current AppImage-specific logic into `upm-appimage`
|
- isolate current AppImage-specific logic into `upm-appimage`
|
||||||
- compose provider behavior in the CLI through `ProviderRegistry` rather than hardcoded AppImage paths in `upm-core`
|
- remove direct AppImage composition from the CLI and move module composition into `upm-core`
|
||||||
- preserve current AppImage functionality and tests during the move
|
- preserve current AppImage functionality and tests during the move
|
||||||
|
|
||||||
Exit criteria:
|
Exit criteria:
|
||||||
|
|
@ -76,18 +80,20 @@ Exit criteria:
|
||||||
|
|
||||||
### Milestone 1: AppImage On The New Core
|
### Milestone 1: AppImage On The New Core
|
||||||
|
|
||||||
Make the current AppImage implementation the first real module on the modular architecture.
|
Make the current AppImage implementation the first real package-manager module on the modular architecture.
|
||||||
|
|
||||||
Goals:
|
Goals:
|
||||||
|
|
||||||
- validate the provider module contract using AppImage as the reference implementation
|
- establish `upm-core` as the sole application boundary for CLI and future GUI frontends
|
||||||
- move search, add, install, update, show, and remove behaviors behind core provider APIs
|
- validate the module contract using AppImage as the reference implementation
|
||||||
- prove the CLI can treat AppImage as just one enabled source
|
- move AppImage-specific acquisition backends behind `upm-appimage`
|
||||||
|
- prove the CLI can treat AppImage as just one enabled module
|
||||||
|
|
||||||
Exit criteria:
|
Exit criteria:
|
||||||
|
|
||||||
- AppImage support is no longer special-cased as the whole product
|
- AppImage support is no longer special-cased as the whole product
|
||||||
- provider registration and capability discovery exist in `upm-core`
|
- CLI command paths run through the `upm-core` application facade
|
||||||
|
- AppImage acquisition minutia no longer lives as top-level application concepts in `upm-core`
|
||||||
|
|
||||||
### Milestone 2: Linux Native Sources
|
### Milestone 2: Linux Native Sources
|
||||||
|
|
||||||
|
|
@ -175,8 +181,8 @@ Exit criteria:
|
||||||
|
|
||||||
The following are explicitly not required to complete Phase 2:
|
The following are explicitly not required to complete Phase 2:
|
||||||
|
|
||||||
- full macOS provider implementation
|
- full macOS module implementation
|
||||||
- Windows provider implementation
|
- Windows module implementation
|
||||||
- GUI frontend delivery
|
- GUI frontend delivery
|
||||||
- forcing strict config-authoritative reconciliation before provider behavior is stable
|
- forcing strict config-authoritative reconciliation before provider behavior is stable
|
||||||
- shipping every conceivable Linux package manager in the first expansion
|
- shipping every conceivable Linux package manager in the first expansion
|
||||||
|
|
@ -199,7 +205,7 @@ That means:
|
||||||
Implementation plans should follow this order:
|
Implementation plans should follow this order:
|
||||||
|
|
||||||
1. rename and crate extraction
|
1. rename and crate extraction
|
||||||
2. provider API definition and AppImage migration onto `upm-core`
|
2. application facade definition and AppImage migration behind a real module boundary
|
||||||
3. Linux provider onboarding in a stable order, likely `pacman` then `Flatpak`, then `AUR`, then `cargo`, then `npm`
|
3. Linux provider onboarding in a stable order, likely `pacman` then `Flatpak`, then `AUR`, then `cargo`, then `npm`
|
||||||
4. ratatui configuration and ranking UX
|
4. ratatui configuration and ranking UX
|
||||||
5. declarative state model, drift detection, and `update` sync behavior
|
5. declarative state model, drift detection, and `update` sync behavior
|
||||||
|
|
|
||||||
|
|
@ -2,57 +2,59 @@
|
||||||
|
|
||||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
**Goal:** Validate AppImage as the first real provider module by making `upm-core` treat provider composition as the normal path for capability discovery, remote show resolution, and update execution.
|
**Goal:** Make `upm-appimage` the first real package-manager module by moving AppImage-specific acquisition paths behind a module boundary and exposing a single application facade from `upm-core` to the CLI and future GUI.
|
||||||
|
|
||||||
**Architecture:** Keep AppImage-specific transport and resolution logic in `upm-appimage`. Extend `upm-core` only with generic provider-registry contracts and plumbing so the CLI can compose providers once and reuse that composition across `search`, `add`, `show`, and `update`. Avoid introducing speculative provider hooks for `remove` or `list`; those flows are already generic and should stay that way until a real provider needs more surface area.
|
**Architecture:** `upm-core` becomes the application boundary. It should own a public facade, internal orchestration services, and module registration and composition. `upm` stays a thin frontend and must stop composing AppImage behavior directly. `upm-appimage` becomes the AppImage package-manager module and should absorb AppImageHub plus the other AppImage-producing backends that are still modeled as top-level source concepts in the core.
|
||||||
|
|
||||||
**Tech Stack:** Rust workspace, `upm`, `upm-core`, `upm-appimage`, Cargo integration tests, fixture-backed provider tests, CLI end-to-end tests.
|
**Tech Stack:** Rust workspace, `upm`, `upm-core`, `upm-appimage`, Cargo integration tests, fixture-backed provider tests, CLI end-to-end tests.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Task 1: Add capability discovery to `ProviderRegistry`
|
### Task 1: Define the public application facade in `upm-core`
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `crates/upm-core/src/app/providers.rs`
|
- Modify: `crates/upm-core/src/app/`
|
||||||
- Modify: `crates/upm-core/src/lib.rs`
|
- Modify: `crates/upm-core/src/lib.rs`
|
||||||
- Test: `crates/upm-core/tests/provider_registry.rs`
|
- Test: `crates/upm-core/tests/`
|
||||||
|
|
||||||
**Step 1: Write the failing capability-discovery expectations**
|
**Step 1: Write the failing facade expectations**
|
||||||
|
|
||||||
Extend `crates/upm-core/tests/provider_registry.rs` to assert:
|
Add focused tests proving that:
|
||||||
|
|
||||||
- the registry can report which provider ids are registered
|
- `upm-core` exposes one public application-facing entrypoint for frontend consumers
|
||||||
- the registry can report whether a provider supports search and/or external add
|
- that entrypoint can be constructed without the CLI owning module composition
|
||||||
- empty registries report no capabilities
|
- the public surface delegates to internal services instead of exposing module wiring details
|
||||||
|
|
||||||
Keep the expectations generic. Do not encode AppImage-only behavior into the registry API.
|
Keep the assertions about API shape and ownership, not AppImage specifics.
|
||||||
|
|
||||||
**Step 2: Run the focused test to verify failure**
|
**Step 2: Run the focused tests to verify failure**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test --package upm-core --test provider_registry
|
cargo test --package upm-core
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: FAIL because `ProviderRegistry` is still only a passive bag of references.
|
Expected: FAIL because the current public surface still assumes narrower provider plumbing and does not expose the intended facade cleanly.
|
||||||
|
|
||||||
**Step 3: Implement minimal capability discovery**
|
**Step 3: Implement the minimal facade**
|
||||||
|
|
||||||
Update `crates/upm-core/src/app/providers.rs` so `ProviderRegistry` exposes a small, stable query surface such as:
|
Introduce or reshape the public API so `upm-core` exposes a single high-level application facade.
|
||||||
|
|
||||||
- a way to enumerate registered provider ids
|
The public boundary should:
|
||||||
- a way to report capabilities for a provider id
|
|
||||||
- a small capability record rather than operation-specific booleans scattered around callers
|
|
||||||
|
|
||||||
Do not add plugin loading, global registries, or dynamic configuration yet.
|
- represent product operations such as search, add, show, update, remove, and config handling
|
||||||
|
- hide module registry and orchestration details from frontends
|
||||||
|
- stay thin and delegate work to internal services
|
||||||
|
|
||||||
**Step 4: Run the focused test to verify pass**
|
Do not add dynamic plugin loading yet.
|
||||||
|
|
||||||
|
**Step 4: Run the focused tests to verify pass**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test --package upm-core --test provider_registry
|
cargo test --package upm-core
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: PASS.
|
Expected: PASS.
|
||||||
|
|
@ -60,113 +62,115 @@ Expected: PASS.
|
||||||
**Step 5: Commit**
|
**Step 5: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add crates/upm-core/src/app/providers.rs crates/upm-core/src/lib.rs crates/upm-core/tests/provider_registry.rs
|
git add crates/upm-core/src crates/upm-core/tests
|
||||||
git commit -m "feat: add provider capability discovery"
|
git commit -m "feat: add application facade to upm-core"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Task 2: Route remote `show` through registered providers
|
### Task 2: Move module composition out of the CLI and into `upm-core`
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
|
- Modify: `crates/upm-core/src/app/`
|
||||||
|
- Modify: `crates/upm/src/lib.rs`
|
||||||
|
- Modify: `crates/upm/src/providers.rs`
|
||||||
|
- Test: `crates/upm-core/tests/`
|
||||||
|
- Test: `crates/upm/tests/end_to_end_cli.rs`
|
||||||
|
|
||||||
|
**Step 1: Write the failing ownership expectations**
|
||||||
|
|
||||||
|
Add coverage proving that:
|
||||||
|
|
||||||
|
- the CLI does not assemble AppImage module composition directly
|
||||||
|
- the application facade can build or receive module composition internally
|
||||||
|
- CLI command paths still behave the same through the new boundary
|
||||||
|
|
||||||
|
Prefer one core ownership test and one CLI integration test.
|
||||||
|
|
||||||
|
**Step 2: Run the focused tests to verify failure**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --package upm --test end_to_end_cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because the CLI still owns direct provider assembly.
|
||||||
|
|
||||||
|
**Step 3: Move composition into `upm-core`**
|
||||||
|
|
||||||
|
Update the architecture so:
|
||||||
|
|
||||||
|
- `upm-core` owns module registration and composition
|
||||||
|
- the CLI constructs the application facade rather than AppImage-specific registries
|
||||||
|
- direct module composition in `crates/upm/src/providers.rs` is removed or reduced to generic application bootstrapping
|
||||||
|
|
||||||
|
Keep CLI UX, rendering, and summary formatting unchanged.
|
||||||
|
|
||||||
|
**Step 4: Run the focused tests to verify pass**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --package upm --test end_to_end_cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
**Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add crates/upm-core/src crates/upm/src/lib.rs crates/upm/src/providers.rs crates/upm/tests/end_to_end_cli.rs
|
||||||
|
git commit -m "refactor: move module composition into upm-core"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Turn `upm-appimage` into the AppImage package-manager boundary
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `crates/upm-appimage/src/`
|
||||||
|
- Modify: `crates/upm-core/src/domain/source.rs`
|
||||||
|
- Modify: `crates/upm-core/src/app/add.rs`
|
||||||
- Modify: `crates/upm-core/src/app/show.rs`
|
- Modify: `crates/upm-core/src/app/show.rs`
|
||||||
- Modify: `crates/upm/src/lib.rs`
|
|
||||||
- Test: `crates/upm-core/tests/show_resolution.rs`
|
|
||||||
- Test: `crates/upm/tests/end_to_end_cli.rs`
|
|
||||||
|
|
||||||
**Step 1: Write the failing `show` expectations**
|
|
||||||
|
|
||||||
Add coverage proving that:
|
|
||||||
|
|
||||||
- remote `show appimagehub/<id>` resolves through the registered provider path
|
|
||||||
- installed-app `show` behavior remains unchanged
|
|
||||||
- unsupported queries still fail distinctly from provider-backed remote queries
|
|
||||||
|
|
||||||
Prefer one new `upm-core` test for remote resolution and one CLI-facing assertion in `crates/upm/tests/end_to_end_cli.rs`.
|
|
||||||
|
|
||||||
**Step 2: Run the focused tests to verify failure**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo test --package upm-core --test show_resolution
|
|
||||||
cargo test --package upm --test end_to_end_cli
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: FAIL because the remote show path still calls the add planner without a `ProviderRegistry`.
|
|
||||||
|
|
||||||
**Step 3: Thread provider composition through `show`**
|
|
||||||
|
|
||||||
Update the show pipeline so:
|
|
||||||
|
|
||||||
- `upm-core` exposes provider-aware show entrypoints alongside the current defaults
|
|
||||||
- remote show resolution uses `build_add_plan_with_registered_providers` rather than the provider-blind path
|
|
||||||
- the CLI wraps remote show dispatch in `providers::with_provider_registry(...)`
|
|
||||||
|
|
||||||
Keep installed-record rendering and summary formatting unchanged.
|
|
||||||
|
|
||||||
**Step 4: Run the focused tests to verify pass**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo test --package upm-core --test show_resolution
|
|
||||||
cargo test --package upm --test end_to_end_cli
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: PASS.
|
|
||||||
|
|
||||||
**Step 5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add crates/upm-core/src/app/show.rs crates/upm/src/lib.rs crates/upm-core/tests/show_resolution.rs crates/upm/tests/end_to_end_cli.rs
|
|
||||||
git commit -m "feat: route show through provider registry"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 3: Route `update` execution through registered providers
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `crates/upm-core/src/app/update.rs`
|
- Modify: `crates/upm-core/src/app/update.rs`
|
||||||
- Modify: `crates/upm/src/lib.rs`
|
- Test: `crates/upm-appimage/tests/`
|
||||||
- Test: `crates/upm-core/tests/update_planning.rs`
|
- Test: `crates/upm-core/tests/`
|
||||||
- Test: `crates/upm/tests/end_to_end_cli.rs`
|
|
||||||
|
|
||||||
**Step 1: Write the failing `update` expectations**
|
**Step 1: Write the failing module-boundary expectations**
|
||||||
|
|
||||||
Add coverage proving that:
|
Add coverage proving that:
|
||||||
|
|
||||||
- AppImage-backed records can be refreshed through the update path with registered providers
|
- AppImage-backed acquisition through GitHub, GitLab, SourceForge, direct URLs, and AppImageHub resolves through `upm-appimage`
|
||||||
- existing GitHub and direct-url update behavior remains unchanged
|
- `upm-core` no longer treats those AppImage-producing backends as top-level package-manager concepts
|
||||||
- the update execution path still restores previous payloads on failure
|
- add, show, and update continue to work through normalized module contracts
|
||||||
|
|
||||||
Prefer focused `upm-core` tests plus one CLI integration assertion for an AppImage-backed update review or execution path.
|
Prefer module-focused tests plus a small number of core integration tests.
|
||||||
|
|
||||||
**Step 2: Run the focused tests to verify failure**
|
**Step 2: Run the focused tests to verify failure**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test --package upm-core --test update_planning
|
cargo test --package upm-appimage
|
||||||
cargo test --package upm --test end_to_end_cli
|
cargo test --package upm-core
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: FAIL because update execution still rebuilds add plans without a `ProviderRegistry`.
|
Expected: FAIL because AppImage acquisition paths are still split between the core and the AppImage module.
|
||||||
|
|
||||||
**Step 3: Thread provider composition through `update`**
|
**Step 3: Move AppImage-specific acquisition logic behind the module**
|
||||||
|
|
||||||
Update the update pipeline so:
|
Reshape the source and module boundary so:
|
||||||
|
|
||||||
- provider-aware update entrypoints exist in `upm-core`
|
- AppImage-specific GitHub, GitLab, SourceForge, AppImageHub, and direct URL handling lives in `upm-appimage`
|
||||||
- `execute_update` rebuilds plans through the provider-aware add planner
|
- `upm-core` coordinates AppImage work through normalized module contracts
|
||||||
- the CLI wraps `update` execution in `providers::with_provider_registry(...)`
|
- core source taxonomy is reduced or reframed so package-manager concepts stay above backend minutia
|
||||||
|
|
||||||
Do not generalize the update-channel model yet. Reuse the current channel semantics and only change how provider-backed plans are rebuilt.
|
Do not over-generalize for Flatpak or future providers yet. Only extract what the AppImage module demonstrably needs.
|
||||||
|
|
||||||
**Step 4: Run the focused tests to verify pass**
|
**Step 4: Run the focused tests to verify pass**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test --package upm-core --test update_planning
|
cargo test --package upm-appimage
|
||||||
cargo test --package upm --test end_to_end_cli
|
cargo test --package upm-core
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: PASS.
|
Expected: PASS.
|
||||||
|
|
@ -174,57 +178,57 @@ Expected: PASS.
|
||||||
**Step 5: Commit**
|
**Step 5: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add crates/upm-core/src/app/update.rs crates/upm/src/lib.rs crates/upm-core/tests/update_planning.rs crates/upm/tests/end_to_end_cli.rs
|
git add crates/upm-appimage/src crates/upm-core/src crates/upm-appimage/tests crates/upm-core/tests
|
||||||
git commit -m "feat: route updates through provider registry"
|
git commit -m "refactor: make upm-appimage the appimage module boundary"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Task 4: Lock AppImage in as the reference provider module
|
### Task 4: Route search, add, show, and update through the application facade
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `crates/upm-appimage/tests/appimagehub_search.rs`
|
- Modify: `crates/upm-core/src/app/`
|
||||||
|
- Modify: `crates/upm/src/lib.rs`
|
||||||
- Modify: `crates/upm/tests/end_to_end_cli.rs`
|
- Modify: `crates/upm/tests/end_to_end_cli.rs`
|
||||||
- Modify: `crates/upm/tests/ui_summary.rs`
|
- Modify: `crates/upm/tests/ui_summary.rs`
|
||||||
- Test: `crates/upm-core/tests/provider_registry.rs`
|
- Test: `crates/upm-core/tests/`
|
||||||
|
|
||||||
**Step 1: Write the failing reference-provider expectations**
|
**Step 1: Write the failing facade-routing expectations**
|
||||||
|
|
||||||
Add end-to-end coverage proving that AppImage support is fully module-driven:
|
Add end-to-end coverage proving that AppImage support is fully module-driven:
|
||||||
|
|
||||||
- `search` still surfaces AppImageHub hits through the registry
|
- `search` flows through the public application facade
|
||||||
- `show` for AppImageHub remote queries works through the registry
|
- `add` flows through the public application facade
|
||||||
- `update` can refresh AppImage-backed records through the registry
|
- `show` flows through the public application facade
|
||||||
|
- `update` flows through the public application facade
|
||||||
- user-facing summaries still render truthful `upm` paths and origins
|
- user-facing summaries still render truthful `upm` paths and origins
|
||||||
|
|
||||||
Keep the assertions focused on module composition rather than UI restyling.
|
Keep the assertions focused on boundary correctness rather than UI restyling.
|
||||||
|
|
||||||
**Step 2: Run the focused tests to verify failure**
|
**Step 2: Run the focused tests to verify failure**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test --package upm-appimage --test appimagehub_search
|
|
||||||
cargo test --package upm --test end_to_end_cli
|
cargo test --package upm --test end_to_end_cli
|
||||||
cargo test --package upm --test ui_summary
|
cargo test --package upm --test ui_summary
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: FAIL until the new `show` and `update` registry plumbing is complete.
|
Expected: FAIL until the facade is the normal command path.
|
||||||
|
|
||||||
**Step 3: Tighten provider-contract validation**
|
**Step 3: Tighten application-boundary validation**
|
||||||
|
|
||||||
Update the tests so they prove:
|
Update the tests so they prove:
|
||||||
|
|
||||||
- AppImage is composed only through `upm-appimage`
|
- frontend command handlers call the application facade rather than module-specific helpers
|
||||||
- `ProviderRegistry` is the shared composition point for all AppImage-facing command paths
|
- AppImage is composed only through `upm-core` and `upm-appimage`
|
||||||
- AppImage is still not reintroduced as a hardcoded built-in inside `upm-core`
|
- AppImage is not reintroduced as a hardcoded built-in in the CLI
|
||||||
|
|
||||||
Do not move AppImageHub back into `all_adapter_kinds()`.
|
Do not reintroduce package-manager-specific branching in the CLI.
|
||||||
|
|
||||||
**Step 4: Run the focused tests to verify pass**
|
**Step 4: Run the focused tests to verify pass**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test --package upm-appimage --test appimagehub_search
|
|
||||||
cargo test --package upm --test end_to_end_cli
|
cargo test --package upm --test end_to_end_cli
|
||||||
cargo test --package upm --test ui_summary
|
cargo test --package upm --test ui_summary
|
||||||
```
|
```
|
||||||
|
|
@ -234,35 +238,35 @@ Expected: PASS.
|
||||||
**Step 5: Commit**
|
**Step 5: Commit**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add crates/upm-appimage/tests/appimagehub_search.rs crates/upm/tests/end_to_end_cli.rs crates/upm/tests/ui_summary.rs crates/upm-core/tests/provider_registry.rs
|
git add crates/upm-core/src crates/upm/src/lib.rs crates/upm/tests/end_to_end_cli.rs crates/upm/tests/ui_summary.rs crates/upm-core/tests
|
||||||
git commit -m "test: validate appimage as reference provider module"
|
git commit -m "refactor: route commands through upm-core facade"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Task 5: Update architecture docs and run full verification
|
### Task 5: Lock the architecture into docs and verify the workspace
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- Modify: `.architecture/overview.md`
|
- Modify: `.architecture/overview.md`
|
||||||
- Modify: `.architecture/roadmap.md`
|
- Modify: `.architecture/roadmap.md`
|
||||||
- Modify: `README.md`
|
- Modify: `README.md`
|
||||||
|
|
||||||
**Step 1: Update docs for Milestone 1 completion state**
|
**Step 1: Update docs for the new module model**
|
||||||
|
|
||||||
Document:
|
Document:
|
||||||
|
|
||||||
- capability discovery in `ProviderRegistry`
|
- `upm-core` as the application boundary
|
||||||
- provider-aware `show` and `update` execution paths
|
- one public application facade over smaller internal services
|
||||||
- AppImage as the first validated provider module on the new architecture
|
- CLI and GUI as thin frontends
|
||||||
- the explicit non-goal that `remove` and `list` remain generic until a provider needs extra hooks
|
- `upm-appimage` as the AppImage package-manager boundary with internal acquisition backends
|
||||||
|
|
||||||
**Step 2: Verify the docs mention the Milestone 1 state**
|
**Step 2: Verify the docs mention the agreed architecture**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rg -n "ProviderRegistry|capabilit|upm-appimage|show|update" README.md .architecture/overview.md .architecture/roadmap.md
|
rg -n "upm-core|facade|upm-ui|upm-appimage|module" README.md .architecture/overview.md .architecture/roadmap.md
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: matches describing the expanded provider surface.
|
Expected: matches describing the application boundary, thin frontends, and module ownership.
|
||||||
|
|
||||||
**Step 3: Run full verification**
|
**Step 3: Run full verification**
|
||||||
|
|
||||||
|
|
@ -280,5 +284,5 @@ Expected: PASS.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add README.md .architecture/overview.md .architecture/roadmap.md
|
git add README.md .architecture/overview.md .architecture/roadmap.md
|
||||||
git commit -m "docs: describe appimage provider milestone"
|
git commit -m "docs: describe application facade architecture"
|
||||||
```
|
```
|
||||||
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -1959,6 +1959,7 @@ dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"upm-core",
|
"upm-core",
|
||||||
|
"upm-module-api",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1975,6 +1976,15 @@ dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"toml",
|
"toml",
|
||||||
|
"upm-appimage",
|
||||||
|
"upm-module-api",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "upm-module-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
"crates/upm-module-api",
|
||||||
"crates/upm-core",
|
"crates/upm-core",
|
||||||
"crates/upm-appimage",
|
"crates/upm-appimage",
|
||||||
"crates/upm",
|
"crates/upm",
|
||||||
|
|
|
||||||
15
README.md
15
README.md
|
|
@ -1,15 +1,16 @@
|
||||||
# upm
|
# upm
|
||||||
Universal Package Manager
|
Universal Package Manager
|
||||||
|
|
||||||
`upm` is a Rust Cargo workspace for a modular package manager with a shared headless core and provider crates.
|
`upm` is a Rust Cargo workspace for a modular package manager with a shared headless application core, thin frontend crates, and package-manager modules.
|
||||||
|
|
||||||
## Workspace
|
## Workspace
|
||||||
|
|
||||||
- `crates/upm-core`: headless application layer for query normalization, resolution, planning, registry persistence, install/update orchestration, and provider-facing APIs
|
- `crates/upm-core`: headless application layer for query normalization, orchestration, module registration and composition, registry persistence, install/update planning, and the unified frontend-facing API
|
||||||
- `crates/upm`: thin terminal frontend for argument parsing, config loading, prompting, progress reporting, and summary rendering
|
- `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`
|
- `crates/upm-appimage`: AppImage package-manager module responsible for AppImage-specific acquisition and resolution behavior
|
||||||
|
- `crates/upm-ui` (planned): GUI frontend over `upm-core`
|
||||||
|
|
||||||
The split is intentional so future frontends can reuse `upm-core`, while package-source behavior stays modular instead of being hardcoded into the core.
|
The split is intentional so future frontends can reuse `upm-core`, while package-manager behavior stays modular instead of being hardcoded into the core or the CLI.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|
@ -37,11 +38,11 @@ upm remove <QUERY>
|
||||||
|
|
||||||
## Search
|
## Search
|
||||||
|
|
||||||
`upm search <QUERY>` is part of the initial modular provider surface.
|
`upm search <QUERY>` is part of the initial modular module surface.
|
||||||
|
|
||||||
- search is provider-extensible and currently includes GitHub plus AppImageHub
|
- search is module-extensible and currently includes the built-in core search path plus AppImage-backed search sources
|
||||||
- search results should resolve to install-ready queries such as `owner/repo` and `appimagehub/<id>`
|
- search results should resolve to install-ready queries such as `owner/repo` and `appimagehub/<id>`
|
||||||
- provider composition happens in `crates/upm/src/providers.rs`, not through AppImageHub-specific wiring inside `upm-core`
|
- module composition belongs in `upm-core`, not in the CLI frontend
|
||||||
|
|
||||||
## Scope Overrides
|
## Scope Overrides
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,7 @@ path = "src/lib.rs"
|
||||||
quick-xml.workspace = true
|
quick-xml.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
upm-module-api = { path = "../upm-module-api" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
upm-core = { path = "../upm-core" }
|
upm-core = { path = "../upm-core" }
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
use crate::source::appimagehub::{
|
use crate::source::appimagehub::{
|
||||||
AppImageHubError, AppImageHubTransport, resolve_appimagehub_item, resolve_appimagehub_item_with,
|
AppImageHubError, AppImageHubTransport, resolve_appimagehub_item, resolve_appimagehub_item_with,
|
||||||
};
|
};
|
||||||
use upm_core::adapters::traits::{
|
use upm_module_api::adapters::traits::{
|
||||||
AdapterCapabilities, AdapterError, AdapterResolution, AdapterResolveOutcome, SourceAdapter,
|
AdapterCapabilities, AdapterError, AdapterResolution, AdapterResolveOutcome, SourceAdapter,
|
||||||
};
|
};
|
||||||
use upm_core::app::providers::{ExternalAddProvider, ExternalAddResolution};
|
use upm_module_api::app::providers::{ExternalAddProvider, ExternalAddResolution};
|
||||||
use upm_core::app::query::resolve_query;
|
use upm_module_api::domain::source::{
|
||||||
use upm_core::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
NormalizedSourceKind, ResolvedRelease, SourceInputKind, SourceKind, SourceRef,
|
||||||
use upm_core::domain::update::{
|
};
|
||||||
|
use upm_module_api::domain::update::{
|
||||||
ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy,
|
ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -58,7 +59,7 @@ impl SourceAdapter for AppImageHubAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
||||||
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
|
let source = resolve_appimagehub_query(query)?;
|
||||||
if source.kind != SourceKind::AppImageHub {
|
if source.kind != SourceKind::AppImageHub {
|
||||||
return Err(AdapterError::UnsupportedQuery);
|
return Err(AdapterError::UnsupportedQuery);
|
||||||
}
|
}
|
||||||
|
|
@ -92,17 +93,17 @@ impl SourceAdapter for AppImageHubAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppImageHubAddProvider<'a, T: AppImageHubTransport + ?Sized> {
|
pub struct AppImageHubAddProvider {
|
||||||
transport: &'a T,
|
transport: Box<dyn AppImageHubTransport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: AppImageHubTransport + ?Sized> AppImageHubAddProvider<'a, T> {
|
impl AppImageHubAddProvider {
|
||||||
pub fn new(transport: &'a T) -> Self {
|
pub fn new(transport: Box<dyn AppImageHubTransport>) -> Self {
|
||||||
Self { transport }
|
Self { transport }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: AppImageHubTransport + ?Sized> ExternalAddProvider for AppImageHubAddProvider<'_, T> {
|
impl ExternalAddProvider for AppImageHubAddProvider {
|
||||||
fn id(&self) -> &'static str {
|
fn id(&self) -> &'static str {
|
||||||
"appimagehub"
|
"appimagehub"
|
||||||
}
|
}
|
||||||
|
|
@ -113,11 +114,11 @@ impl<T: AppImageHubTransport + ?Sized> ExternalAddProvider for AppImageHubAddPro
|
||||||
}
|
}
|
||||||
|
|
||||||
let adapter = AppImageHubAdapter;
|
let adapter = AppImageHubAdapter;
|
||||||
let resolution = match adapter.resolve_source_with(source, self.transport)? {
|
let resolution = match adapter.resolve_source_with(source, self.transport.as_ref())? {
|
||||||
AdapterResolveOutcome::Resolved(resolution) => resolution,
|
AdapterResolveOutcome::Resolved(resolution) => resolution,
|
||||||
AdapterResolveOutcome::NoInstallableArtifact { .. } => return Ok(None),
|
AdapterResolveOutcome::NoInstallableArtifact { .. } => return Ok(None),
|
||||||
};
|
};
|
||||||
let Some(resolved_item) = resolve_appimagehub_item_with(source, self.transport)
|
let Some(resolved_item) = resolve_appimagehub_item_with(source, self.transport.as_ref())
|
||||||
.map_err(|error| AdapterError::ResolutionFailed(format!("{error:?}")))?
|
.map_err(|error| AdapterError::ResolutionFailed(format!("{error:?}")))?
|
||||||
else {
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|
@ -161,3 +162,35 @@ fn render_appimagehub_error(error: &AppImageHubError) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_appimagehub_query(query: &str) -> Result<SourceRef, AdapterError> {
|
||||||
|
let trimmed = query.trim();
|
||||||
|
let id = if let Some(id) = trimmed.strip_prefix("appimagehub/") {
|
||||||
|
id
|
||||||
|
} else if let Some(id) = trimmed.strip_prefix("https://www.appimagehub.com/p/") {
|
||||||
|
id
|
||||||
|
} else if let Some(id) = trimmed.strip_prefix("http://www.appimagehub.com/p/") {
|
||||||
|
id
|
||||||
|
} else {
|
||||||
|
return Err(AdapterError::UnsupportedQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !id.chars().all(|ch| ch.is_ascii_digit()) {
|
||||||
|
return Err(AdapterError::UnsupportedQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SourceRef {
|
||||||
|
kind: SourceKind::AppImageHub,
|
||||||
|
locator: format!("https://www.appimagehub.com/p/{id}"),
|
||||||
|
input_kind: if trimmed.starts_with("appimagehub/") {
|
||||||
|
SourceInputKind::AppImageHubShorthand
|
||||||
|
} else {
|
||||||
|
SourceInputKind::AppImageHubUrl
|
||||||
|
},
|
||||||
|
normalized_kind: NormalizedSourceKind::AppImageHub,
|
||||||
|
canonical_locator: Some(id.to_owned()),
|
||||||
|
requested_tag: None,
|
||||||
|
requested_asset_name: None,
|
||||||
|
tracks_latest: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,29 @@
|
||||||
use crate::source::appimagehub::{
|
use crate::source::appimagehub::{
|
||||||
AppImageHubSearchError, AppImageHubTransport, search_appimagehub_with,
|
AppImageHubSearchError, AppImageHubTransport, search_appimagehub_with,
|
||||||
};
|
};
|
||||||
use upm_core::app::search::{SearchProvider, SearchProviderError};
|
use upm_module_api::app::search::{SearchProvider, SearchProviderError};
|
||||||
use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
|
use upm_module_api::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
|
||||||
|
|
||||||
pub struct AppImageHubSearchProvider<'a, T: AppImageHubTransport + ?Sized> {
|
pub struct AppImageHubSearchProvider {
|
||||||
transport: &'a T,
|
transport: Box<dyn AppImageHubTransport>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T: AppImageHubTransport + ?Sized> AppImageHubSearchProvider<'a, T> {
|
impl AppImageHubSearchProvider {
|
||||||
pub fn new(transport: &'a T) -> Self {
|
pub fn new(transport: Box<dyn AppImageHubTransport>) -> Self {
|
||||||
Self { transport }
|
Self { transport }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: AppImageHubTransport + ?Sized> SearchProvider for AppImageHubSearchProvider<'_, T> {
|
impl SearchProvider for AppImageHubSearchProvider {
|
||||||
fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
|
fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
|
||||||
let hits = search_appimagehub_with(&query.text, query.remote_limit, self.transport)
|
let hits =
|
||||||
.map_err(|error| {
|
search_appimagehub_with(&query.text, query.remote_limit, self.transport.as_ref())
|
||||||
SearchProviderError::new("appimagehub", &render_appimagehub_search_error(&error))
|
.map_err(|error| {
|
||||||
})?;
|
SearchProviderError::new(
|
||||||
|
"appimagehub",
|
||||||
|
&render_appimagehub_search_error(&error),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let normalized_query = normalize_lookup(&query.text);
|
let normalized_query = normalize_lookup(&query.text);
|
||||||
let mut ranked_hits = hits
|
let mut ranked_hits = hits
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use upm_core::domain::source::SourceRef;
|
use upm_module_api::domain::source::SourceRef;
|
||||||
|
|
||||||
const DEFAULT_APPIMAGEHUB_API_BASE: &str = "https://api.appimagehub.com/ocs/v1/content";
|
const DEFAULT_APPIMAGEHUB_API_BASE: &str = "https://api.appimagehub.com/ocs/v1/content";
|
||||||
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
|
const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE";
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ impl SearchProvider for StubProvider {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn appimagehub_search_provider_maps_hits_to_install_ready_results() {
|
fn appimagehub_search_provider_maps_hits_to_install_ready_results() {
|
||||||
let provider = AppImageHubSearchProvider::new(&FixtureAppImageHubTransport);
|
let provider = AppImageHubSearchProvider::new(Box::new(FixtureAppImageHubTransport));
|
||||||
|
|
||||||
let results = provider.search(&SearchQuery::new("firefox")).unwrap();
|
let results = provider.search(&SearchQuery::new("firefox")).unwrap();
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ fn appimagehub_search_provider_maps_hits_to_install_ready_results() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn appimagehub_hits_are_annotated_as_installed_by_canonical_id() {
|
fn appimagehub_hits_are_annotated_as_installed_by_canonical_id() {
|
||||||
let provider = AppImageHubSearchProvider::new(&FixtureAppImageHubTransport);
|
let provider = AppImageHubSearchProvider::new(Box::new(FixtureAppImageHubTransport));
|
||||||
let installed = vec![AppRecord {
|
let installed = vec![AppRecord {
|
||||||
stable_id: "firefox".to_owned(),
|
stable_id: "firefox".to_owned(),
|
||||||
display_name: "Firefox by Mozilla - Official AppImage Edition".to_owned(),
|
display_name: "Firefox by Mozilla - Official AppImage Edition".to_owned(),
|
||||||
|
|
@ -76,7 +76,7 @@ fn appimagehub_hits_are_annotated_as_installed_by_canonical_id() {
|
||||||
#[test]
|
#[test]
|
||||||
fn search_can_merge_github_and_appimagehub_providers() {
|
fn search_can_merge_github_and_appimagehub_providers() {
|
||||||
let github = GitHubSearchProvider::new(&FixtureGitHubTransport);
|
let github = GitHubSearchProvider::new(&FixtureGitHubTransport);
|
||||||
let appimagehub = AppImageHubSearchProvider::new(&FixtureAppImageHubTransport);
|
let appimagehub = AppImageHubSearchProvider::new(Box::new(FixtureAppImageHubTransport));
|
||||||
let stub = StubProvider {
|
let stub = StubProvider {
|
||||||
hit: SearchResult {
|
hit: SearchResult {
|
||||||
provider_id: "github".to_owned(),
|
provider_id: "github".to_owned(),
|
||||||
|
|
@ -131,7 +131,7 @@ fn appimagehub_adapter_resolves_installable_items_through_fixture_transport() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn appimagehub_add_provider_resolves_external_add_plan() {
|
fn appimagehub_add_provider_resolves_external_add_plan() {
|
||||||
let provider = AppImageHubAddProvider::new(&FixtureAppImageHubTransport);
|
let provider = AppImageHubAddProvider::new(Box::new(FixtureAppImageHubTransport));
|
||||||
let source = resolve_query("appimagehub/2338455").unwrap();
|
let source = resolve_query("appimagehub/2338455").unwrap();
|
||||||
|
|
||||||
let resolution = provider.resolve(&source).unwrap().unwrap();
|
let resolution = provider.resolve(&source).unwrap().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ serde.workspace = true
|
||||||
serde_yaml.workspace = true
|
serde_yaml.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
|
upm-appimage = { path = "../upm-appimage" }
|
||||||
|
upm-module-api = { path = "../upm-module-api" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1 @@
|
||||||
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
pub use upm_module_api::adapters::traits::*;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub struct AdapterCapabilities {
|
|
||||||
pub supports_search: bool,
|
|
||||||
pub supports_exact_resolution: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AdapterCapabilities {
|
|
||||||
pub fn exact_resolution_only() -> Self {
|
|
||||||
Self {
|
|
||||||
supports_search: false,
|
|
||||||
supports_exact_resolution: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct AdapterResolution {
|
|
||||||
pub source: SourceRef,
|
|
||||||
pub release: ResolvedRelease,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub enum AdapterResolveOutcome {
|
|
||||||
Resolved(AdapterResolution),
|
|
||||||
NoInstallableArtifact { source: SourceRef },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub enum AdapterError {
|
|
||||||
UnsupportedQuery,
|
|
||||||
UnsupportedSource,
|
|
||||||
ResolutionFailed(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait SourceAdapter {
|
|
||||||
fn id(&self) -> &'static str;
|
|
||||||
|
|
||||||
fn capabilities(&self) -> AdapterCapabilities;
|
|
||||||
|
|
||||||
fn repository_source_kind(&self) -> Option<SourceKind> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exact_source_kind(&self) -> Option<SourceKind> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError>;
|
|
||||||
|
|
||||||
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError>;
|
|
||||||
|
|
||||||
fn resolve_supported_source(
|
|
||||||
&self,
|
|
||||||
source: &SourceRef,
|
|
||||||
) -> Result<AdapterResolveOutcome, AdapterError> {
|
|
||||||
self.resolve(source).map(AdapterResolveOutcome::Resolved)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supports_source(&self, source: &SourceRef) -> bool {
|
|
||||||
crate::adapters::supports_source(self, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_source(&self, source: &SourceRef) -> Result<AdapterResolveOutcome, AdapterError> {
|
|
||||||
if !self.supports_source(source) {
|
|
||||||
return Err(AdapterError::UnsupportedSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.resolve_supported_source(source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
182
crates/upm-core/src/app/application.rs
Normal file
182
crates/upm-core/src/app/application.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::app::add::{
|
||||||
|
AddPlan, AddSecurityPolicy, BuildAddPlanError, InstalledApp,
|
||||||
|
build_add_plan_with_registered_providers,
|
||||||
|
build_add_plan_with_reporter_and_registered_providers, install_app_with_reporter,
|
||||||
|
};
|
||||||
|
use crate::app::list::{ListRow, build_list_rows};
|
||||||
|
use crate::app::progress::ProgressReporter;
|
||||||
|
use crate::app::providers::ProviderRegistry;
|
||||||
|
use crate::app::remove::{RemovalResult, RemoveRegisteredAppError, remove_registered_app};
|
||||||
|
use crate::app::search::{SearchError, SearchProvider, build_search_results_with};
|
||||||
|
use crate::app::show::build_show_result_with;
|
||||||
|
use crate::app::update::{
|
||||||
|
BuildUpdatePlanError, ExecuteUpdatesError, build_update_plan,
|
||||||
|
execute_updates_with_reporter_and_policy,
|
||||||
|
};
|
||||||
|
use crate::domain::app::{AppRecord, InstallScope};
|
||||||
|
use crate::domain::search::{SearchQuery, SearchResults};
|
||||||
|
use crate::domain::show::{InstalledShow, ShowResult, ShowResultError};
|
||||||
|
use crate::domain::update::{UpdateExecutionResult, UpdatePlan};
|
||||||
|
use crate::source::github::{GitHubTransport, default_transport};
|
||||||
|
|
||||||
|
pub struct UpmApp<'a> {
|
||||||
|
github_transport: Box<dyn GitHubTransport>,
|
||||||
|
providers: ProviderRegistry<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UpmAppBuilder<'a> {
|
||||||
|
github_transport: Option<Box<dyn GitHubTransport>>,
|
||||||
|
providers: ProviderRegistry<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpmApp<'static> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::builder()
|
||||||
|
.with_provider_registry(default_provider_registry())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UpmApp<'static> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> UpmApp<'a> {
|
||||||
|
pub fn builder() -> UpmAppBuilder<'a> {
|
||||||
|
UpmAppBuilder {
|
||||||
|
github_transport: None,
|
||||||
|
providers: ProviderRegistry::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search(
|
||||||
|
&self,
|
||||||
|
query: &SearchQuery,
|
||||||
|
installed_apps: &[AppRecord],
|
||||||
|
) -> Result<SearchResults, SearchError> {
|
||||||
|
let github_provider =
|
||||||
|
crate::app::search::GitHubSearchProvider::new(self.github_transport.as_ref());
|
||||||
|
let mut resolved_providers = vec![&github_provider as &dyn SearchProvider];
|
||||||
|
resolved_providers.extend(
|
||||||
|
self.providers
|
||||||
|
.search_providers
|
||||||
|
.iter()
|
||||||
|
.map(|provider| provider.as_ref() as &dyn SearchProvider),
|
||||||
|
);
|
||||||
|
build_search_results_with(query, installed_apps, &resolved_providers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_add_plan(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
policy: AddSecurityPolicy,
|
||||||
|
) -> Result<AddPlan, BuildAddPlanError> {
|
||||||
|
build_add_plan_with_registered_providers(
|
||||||
|
query,
|
||||||
|
self.github_transport.as_ref(),
|
||||||
|
&self.providers,
|
||||||
|
policy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_add_plan_with_reporter(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
reporter: &mut impl ProgressReporter,
|
||||||
|
policy: AddSecurityPolicy,
|
||||||
|
) -> Result<AddPlan, BuildAddPlanError> {
|
||||||
|
build_add_plan_with_reporter_and_registered_providers(
|
||||||
|
query,
|
||||||
|
self.github_transport.as_ref(),
|
||||||
|
reporter,
|
||||||
|
&self.providers,
|
||||||
|
policy,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install_app(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
plan: &AddPlan,
|
||||||
|
install_home: &Path,
|
||||||
|
requested_scope: InstallScope,
|
||||||
|
reporter: &mut impl ProgressReporter,
|
||||||
|
) -> Result<InstalledApp, crate::app::add::InstallAppError> {
|
||||||
|
install_app_with_reporter(query, plan, install_home, requested_scope, reporter)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
installed_apps: &[AppRecord],
|
||||||
|
) -> Result<ShowResult, ShowResultError> {
|
||||||
|
build_show_result_with(query, installed_apps, self.github_transport.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_all(&self, installed_apps: &[AppRecord]) -> Vec<InstalledShow> {
|
||||||
|
crate::app::show::build_installed_show_results(installed_apps)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self, apps: &[AppRecord]) -> Vec<ListRow> {
|
||||||
|
build_list_rows(apps)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_update_plan(
|
||||||
|
&self,
|
||||||
|
apps: &[AppRecord],
|
||||||
|
) -> Result<UpdatePlan, BuildUpdatePlanError> {
|
||||||
|
build_update_plan(apps)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_updates(
|
||||||
|
&self,
|
||||||
|
apps: &[AppRecord],
|
||||||
|
install_home: &Path,
|
||||||
|
reporter: &mut impl ProgressReporter,
|
||||||
|
policy: AddSecurityPolicy,
|
||||||
|
) -> Result<UpdateExecutionResult, ExecuteUpdatesError> {
|
||||||
|
execute_updates_with_reporter_and_policy(apps, install_home, reporter, policy)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_registered_app(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
apps: &[AppRecord],
|
||||||
|
install_home: &Path,
|
||||||
|
) -> Result<RemovalResult, RemoveRegisteredAppError> {
|
||||||
|
remove_registered_app(query, apps, install_home)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> UpmAppBuilder<'a> {
|
||||||
|
pub fn with_github_transport(mut self, github_transport: Box<dyn GitHubTransport>) -> Self {
|
||||||
|
self.github_transport = Some(github_transport);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_provider_registry(mut self, providers: ProviderRegistry<'a>) -> Self {
|
||||||
|
self.providers = providers;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(self) -> UpmApp<'a> {
|
||||||
|
UpmApp {
|
||||||
|
github_transport: self.github_transport.unwrap_or_else(default_transport),
|
||||||
|
providers: self.providers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_provider_registry() -> ProviderRegistry<'static> {
|
||||||
|
ProviderRegistry::default()
|
||||||
|
.with_search_provider(upm_appimage::AppImageHubSearchProvider::new(
|
||||||
|
upm_appimage::source::appimagehub::default_transport(),
|
||||||
|
))
|
||||||
|
.with_external_add_provider(upm_appimage::AppImageHubAddProvider::new(
|
||||||
|
upm_appimage::source::appimagehub::default_transport(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod add;
|
pub mod add;
|
||||||
|
pub mod application;
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
|
@ -10,3 +11,5 @@ pub mod scope;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod show;
|
pub mod show;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
|
||||||
|
pub use application::{UpmApp, UpmAppBuilder};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1 @@
|
||||||
use crate::adapters::traits::{AdapterError, AdapterResolution};
|
pub use upm_module_api::app::providers::*;
|
||||||
use crate::app::search::SearchProvider;
|
|
||||||
use crate::domain::source::SourceRef;
|
|
||||||
use crate::domain::update::{ArtifactCandidate, UpdateStrategy};
|
|
||||||
|
|
||||||
pub trait ExternalAddProvider {
|
|
||||||
fn id(&self) -> &'static str;
|
|
||||||
|
|
||||||
fn resolve(&self, source: &SourceRef) -> Result<Option<ExternalAddResolution>, AdapterError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct ExternalAddResolution {
|
|
||||||
pub resolution: AdapterResolution,
|
|
||||||
pub selected_artifact: ArtifactCandidate,
|
|
||||||
pub update_strategy: UpdateStrategy,
|
|
||||||
pub display_name_hint: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct ProviderRegistry<'a> {
|
|
||||||
pub search_providers: Vec<&'a dyn SearchProvider>,
|
|
||||||
pub external_add_providers: Vec<&'a dyn ExternalAddProvider>,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -9,25 +9,7 @@ use crate::source::github::{
|
||||||
search_github_repositories_with,
|
search_github_repositories_with,
|
||||||
};
|
};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
pub use upm_module_api::app::search::{SearchProvider, SearchProviderError};
|
||||||
pub trait SearchProvider {
|
|
||||||
fn search(&self, query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct SearchProviderError {
|
|
||||||
pub provider_id: String,
|
|
||||||
pub message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchProviderError {
|
|
||||||
pub fn new(provider_id: &str, message: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
provider_id: provider_id.to_owned(),
|
|
||||||
message: message.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum SearchError {
|
pub enum SearchError {
|
||||||
|
|
@ -53,7 +35,12 @@ pub fn build_search_results_with_registered_providers(
|
||||||
let github_transport = default_transport();
|
let github_transport = default_transport();
|
||||||
let github_provider = GitHubSearchProvider::new(github_transport.as_ref());
|
let github_provider = GitHubSearchProvider::new(github_transport.as_ref());
|
||||||
let mut resolved_providers = vec![&github_provider as &dyn SearchProvider];
|
let mut resolved_providers = vec![&github_provider as &dyn SearchProvider];
|
||||||
resolved_providers.extend(providers.search_providers.iter().copied());
|
resolved_providers.extend(
|
||||||
|
providers
|
||||||
|
.search_providers
|
||||||
|
.iter()
|
||||||
|
.map(|provider| provider.as_ref() as &dyn SearchProvider),
|
||||||
|
);
|
||||||
|
|
||||||
build_search_results_with(query, installed_apps, &resolved_providers)
|
build_search_results_with(query, installed_apps, &resolved_providers)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1 @@
|
||||||
pub const DEFAULT_REMOTE_LIMIT: usize = 10;
|
pub use upm_module_api::domain::search::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub enum SearchInstallStatus {
|
|
||||||
Available,
|
|
||||||
Installed {
|
|
||||||
installed_version: Option<String>,
|
|
||||||
},
|
|
||||||
UpdateAvailable {
|
|
||||||
installed_version: Option<String>,
|
|
||||||
latest_version: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct SearchQuery {
|
|
||||||
pub text: String,
|
|
||||||
pub remote_limit: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SearchQuery {
|
|
||||||
pub fn new(text: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
text: text.to_owned(),
|
|
||||||
remote_limit: DEFAULT_REMOTE_LIMIT,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_remote_limit(text: &str, remote_limit: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
text: text.to_owned(),
|
|
||||||
remote_limit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct SearchResult {
|
|
||||||
pub provider_id: String,
|
|
||||||
pub display_name: String,
|
|
||||||
pub description: Option<String>,
|
|
||||||
pub source_locator: String,
|
|
||||||
pub install_query: String,
|
|
||||||
pub canonical_locator: String,
|
|
||||||
pub version: Option<String>,
|
|
||||||
pub install_status: SearchInstallStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct InstalledSearchMatch {
|
|
||||||
pub stable_id: String,
|
|
||||||
pub display_name: String,
|
|
||||||
pub installed_version: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct SearchWarning {
|
|
||||||
pub provider_id: Option<String>,
|
|
||||||
pub message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct SearchResults {
|
|
||||||
pub query_text: String,
|
|
||||||
pub remote_hits: Vec<SearchResult>,
|
|
||||||
pub installed_matches: Vec<InstalledSearchMatch>,
|
|
||||||
pub warnings: Vec<SearchWarning>,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,117 +1 @@
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
pub use upm_module_api::domain::source::*;
|
||||||
pub enum SourceKind {
|
|
||||||
GitHub,
|
|
||||||
GitLab,
|
|
||||||
AppImageHub,
|
|
||||||
SourceForge,
|
|
||||||
DirectUrl,
|
|
||||||
File,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceKind {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::GitHub => "github",
|
|
||||||
Self::GitLab => "gitlab",
|
|
||||||
Self::AppImageHub => "appimagehub",
|
|
||||||
Self::SourceForge => "sourceforge",
|
|
||||||
Self::DirectUrl => "direct-url",
|
|
||||||
Self::File => "file",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub enum SourceInputKind {
|
|
||||||
RepoShorthand,
|
|
||||||
GitHubRepositoryUrl,
|
|
||||||
GitHubReleaseUrl,
|
|
||||||
GitHubReleaseAssetUrl,
|
|
||||||
GitLabUrl,
|
|
||||||
AppImageHubUrl,
|
|
||||||
AppImageHubShorthand,
|
|
||||||
SourceForgeUrl,
|
|
||||||
DirectUrl,
|
|
||||||
File,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceInputKind {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::RepoShorthand => "repo-shorthand",
|
|
||||||
Self::GitHubRepositoryUrl => "github-repository-url",
|
|
||||||
Self::GitHubReleaseUrl => "github-release-url",
|
|
||||||
Self::GitHubReleaseAssetUrl => "github-release-asset-url",
|
|
||||||
Self::GitLabUrl => "gitlab-url",
|
|
||||||
Self::AppImageHubUrl => "appimagehub-url",
|
|
||||||
Self::AppImageHubShorthand => "appimagehub-shorthand",
|
|
||||||
Self::SourceForgeUrl => "sourceforge-url",
|
|
||||||
Self::DirectUrl => "direct-url",
|
|
||||||
Self::File => "file",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub enum NormalizedSourceKind {
|
|
||||||
GitHubRepository,
|
|
||||||
GitHubRelease,
|
|
||||||
GitHubReleaseAsset,
|
|
||||||
GitLab,
|
|
||||||
GitLabCandidate,
|
|
||||||
AppImageHub,
|
|
||||||
SourceForge,
|
|
||||||
SourceForgeCandidate,
|
|
||||||
DirectUrl,
|
|
||||||
File,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NormalizedSourceKind {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::GitHubRepository => "github-repository",
|
|
||||||
Self::GitHubRelease => "github-release",
|
|
||||||
Self::GitHubReleaseAsset => "github-release-asset",
|
|
||||||
Self::GitLab => "gitlab",
|
|
||||||
Self::GitLabCandidate => "gitlab-candidate",
|
|
||||||
Self::AppImageHub => "appimagehub",
|
|
||||||
Self::SourceForge => "sourceforge",
|
|
||||||
Self::SourceForgeCandidate => "sourceforge-candidate",
|
|
||||||
Self::DirectUrl => "direct-url",
|
|
||||||
Self::File => "file",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct SourceRef {
|
|
||||||
pub kind: SourceKind,
|
|
||||||
pub locator: String,
|
|
||||||
#[serde(default = "default_source_input_kind")]
|
|
||||||
pub input_kind: SourceInputKind,
|
|
||||||
#[serde(default = "default_normalized_source_kind")]
|
|
||||||
pub normalized_kind: NormalizedSourceKind,
|
|
||||||
#[serde(default)]
|
|
||||||
pub canonical_locator: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub requested_tag: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub requested_asset_name: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub tracks_latest: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct ResolvedRelease {
|
|
||||||
pub version: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub prerelease: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_source_input_kind() -> SourceInputKind {
|
|
||||||
SourceInputKind::DirectUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
const fn default_normalized_source_kind() -> NormalizedSourceKind {
|
|
||||||
NormalizedSourceKind::DirectUrl
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
use crate::domain::app::AppRecord;
|
use crate::domain::app::AppRecord;
|
||||||
|
pub use upm_module_api::domain::update::{
|
||||||
|
ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
pub enum ParsedMetadataKind {
|
pub enum ParsedMetadataKind {
|
||||||
|
|
@ -31,25 +34,6 @@ pub struct ParsedMetadata {
|
||||||
pub confidence: u8,
|
pub confidence: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub enum UpdateChannelKind {
|
|
||||||
GitHubReleases,
|
|
||||||
ElectronBuilder,
|
|
||||||
Zsync,
|
|
||||||
DirectAsset,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UpdateChannelKind {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::GitHubReleases => "github-releases",
|
|
||||||
Self::ElectronBuilder => "electron-builder",
|
|
||||||
Self::Zsync => "zsync",
|
|
||||||
Self::DirectAsset => "direct-asset-lineage",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct UpdateChannel {
|
pub struct UpdateChannel {
|
||||||
pub kind: UpdateChannelKind,
|
pub kind: UpdateChannelKind,
|
||||||
|
|
@ -66,30 +50,6 @@ pub struct UpdateChannel {
|
||||||
pub prerelease: bool,
|
pub prerelease: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct ChannelPreference {
|
|
||||||
pub kind: UpdateChannelKind,
|
|
||||||
pub locator: String,
|
|
||||||
pub reason: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
||||||
pub struct UpdateStrategy {
|
|
||||||
pub preferred: ChannelPreference,
|
|
||||||
#[serde(default)]
|
|
||||||
pub alternates: Vec<ChannelPreference>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct ArtifactCandidate {
|
|
||||||
pub url: String,
|
|
||||||
pub version: String,
|
|
||||||
pub arch: Option<String>,
|
|
||||||
pub trusted_checksum: Option<String>,
|
|
||||||
pub weak_checksum_md5: Option<String>,
|
|
||||||
pub selection_reason: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct UpdatePlan {
|
pub struct UpdatePlan {
|
||||||
pub items: Vec<PlannedUpdate>,
|
pub items: Vec<PlannedUpdate>,
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,4 @@ pub mod source;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
|
||||||
pub use app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry};
|
pub use app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry};
|
||||||
|
pub use app::{UpmApp, UpmAppBuilder};
|
||||||
|
|
|
||||||
124
crates/upm-core/tests/application_facade.rs
Normal file
124
crates/upm-core/tests/application_facade.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
use upm_core::adapters::traits::AdapterResolution;
|
||||||
|
use upm_core::app::UpmApp;
|
||||||
|
use upm_core::app::add::AddSecurityPolicy;
|
||||||
|
use upm_core::app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry};
|
||||||
|
use upm_core::app::search::{SearchProvider, SearchProviderError};
|
||||||
|
use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult};
|
||||||
|
use upm_core::domain::source::{
|
||||||
|
NormalizedSourceKind, ResolvedRelease, SourceInputKind, SourceKind, SourceRef,
|
||||||
|
};
|
||||||
|
use upm_core::domain::update::{
|
||||||
|
ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy,
|
||||||
|
};
|
||||||
|
use upm_core::source::github::FixtureGitHubTransport;
|
||||||
|
|
||||||
|
struct StubSearchProvider;
|
||||||
|
|
||||||
|
impl SearchProvider for StubSearchProvider {
|
||||||
|
fn search(&self, _query: &SearchQuery) -> Result<Vec<SearchResult>, SearchProviderError> {
|
||||||
|
Ok(vec![SearchResult {
|
||||||
|
provider_id: "external-search".to_owned(),
|
||||||
|
display_name: "Firefox Nightly".to_owned(),
|
||||||
|
description: Some("Provided by facade-owned providers".to_owned()),
|
||||||
|
source_locator: "https://example.invalid/firefox-nightly".to_owned(),
|
||||||
|
install_query: "external/firefox-nightly".to_owned(),
|
||||||
|
canonical_locator: "external/firefox-nightly".to_owned(),
|
||||||
|
version: Some("2026.03.21".to_owned()),
|
||||||
|
install_status: SearchInstallStatus::Available,
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StubExternalAddProvider;
|
||||||
|
|
||||||
|
impl ExternalAddProvider for StubExternalAddProvider {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"stub-appimage"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve(
|
||||||
|
&self,
|
||||||
|
source: &SourceRef,
|
||||||
|
) -> Result<Option<ExternalAddResolution>, upm_core::adapters::traits::AdapterError> {
|
||||||
|
Ok(
|
||||||
|
(source.kind == SourceKind::AppImageHub).then(|| ExternalAddResolution {
|
||||||
|
resolution: 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 upm_app_can_be_constructed_without_cli_owned_module_composition() {
|
||||||
|
let _app = UpmApp::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn upm_app_search_delegates_through_the_application_facade() {
|
||||||
|
let app = UpmApp::builder()
|
||||||
|
.with_github_transport(Box::new(FixtureGitHubTransport))
|
||||||
|
.with_provider_registry(
|
||||||
|
ProviderRegistry::default().with_search_provider(StubSearchProvider),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let results = app.search(&SearchQuery::new("firefox"), &[]).unwrap();
|
||||||
|
|
||||||
|
assert!(results.remote_hits.iter().any(|hit| {
|
||||||
|
hit.provider_id == "external-search" && hit.install_query == "external/firefox-nightly"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn upm_app_add_planning_delegates_through_the_application_facade() {
|
||||||
|
let app = UpmApp::builder()
|
||||||
|
.with_github_transport(Box::new(FixtureGitHubTransport))
|
||||||
|
.with_provider_registry(
|
||||||
|
ProviderRegistry::default().with_external_add_provider(StubExternalAddProvider),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let plan = app
|
||||||
|
.build_add_plan("appimagehub/2338455", AddSecurityPolicy::default())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(plan.resolution.source.kind, SourceKind::AppImageHub);
|
||||||
|
assert_eq!(
|
||||||
|
plan.selected_artifact.url,
|
||||||
|
"https://downloads.example.invalid/firefox.AppImage"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -86,11 +86,7 @@ impl ExternalAddProvider for StubExternalAddProvider {
|
||||||
#[test]
|
#[test]
|
||||||
fn build_search_results_with_registered_providers_uses_external_hits() {
|
fn build_search_results_with_registered_providers_uses_external_hits() {
|
||||||
let query = SearchQuery::new("firefox");
|
let query = SearchQuery::new("firefox");
|
||||||
let search_provider = StubSearchProvider;
|
let providers = ProviderRegistry::default().with_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 results = build_search_results_with_registered_providers(&query, &[], &providers).unwrap();
|
||||||
|
|
||||||
|
|
@ -129,11 +125,7 @@ fn build_add_plan_with_registered_providers_requires_external_provider_for_appim
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_add_plan_with_registered_providers_delegates_appimagehub_like_sources() {
|
fn build_add_plan_with_registered_providers_delegates_appimagehub_like_sources() {
|
||||||
let provider = StubExternalAddProvider;
|
let registry = ProviderRegistry::default().with_external_add_provider(StubExternalAddProvider);
|
||||||
let registry = ProviderRegistry {
|
|
||||||
search_providers: Vec::new(),
|
|
||||||
external_add_providers: vec![&provider],
|
|
||||||
};
|
|
||||||
|
|
||||||
let plan = build_add_plan_with_registered_providers(
|
let plan = build_add_plan_with_registered_providers(
|
||||||
"appimagehub/2338455",
|
"appimagehub/2338455",
|
||||||
|
|
|
||||||
11
crates/upm-module-api/Cargo.toml
Normal file
11
crates/upm-module-api/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "upm-module-api"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde.workspace = true
|
||||||
1
crates/upm-module-api/src/adapters/mod.rs
Normal file
1
crates/upm-module-api/src/adapters/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod traits;
|
||||||
73
crates/upm-module-api/src/adapters/traits.rs
Normal file
73
crates/upm-module-api/src/adapters/traits.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct AdapterCapabilities {
|
||||||
|
pub supports_search: bool,
|
||||||
|
pub supports_exact_resolution: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdapterCapabilities {
|
||||||
|
pub fn exact_resolution_only() -> Self {
|
||||||
|
Self {
|
||||||
|
supports_search: false,
|
||||||
|
supports_exact_resolution: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct AdapterResolution {
|
||||||
|
pub source: SourceRef,
|
||||||
|
pub release: ResolvedRelease,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum AdapterResolveOutcome {
|
||||||
|
Resolved(AdapterResolution),
|
||||||
|
NoInstallableArtifact { source: SourceRef },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum AdapterError {
|
||||||
|
UnsupportedQuery,
|
||||||
|
UnsupportedSource,
|
||||||
|
ResolutionFailed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SourceAdapter {
|
||||||
|
fn id(&self) -> &'static str;
|
||||||
|
|
||||||
|
fn capabilities(&self) -> AdapterCapabilities;
|
||||||
|
|
||||||
|
fn repository_source_kind(&self) -> Option<SourceKind> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exact_source_kind(&self) -> Option<SourceKind> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError>;
|
||||||
|
|
||||||
|
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError>;
|
||||||
|
|
||||||
|
fn resolve_supported_source(
|
||||||
|
&self,
|
||||||
|
source: &SourceRef,
|
||||||
|
) -> Result<AdapterResolveOutcome, AdapterError> {
|
||||||
|
self.resolve(source).map(AdapterResolveOutcome::Resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_source(&self, source: &SourceRef) -> bool {
|
||||||
|
self.repository_source_kind() == Some(source.kind)
|
||||||
|
|| self.exact_source_kind() == Some(source.kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_source(&self, source: &SourceRef) -> Result<AdapterResolveOutcome, AdapterError> {
|
||||||
|
if !self.supports_source(source) {
|
||||||
|
return Err(AdapterError::UnsupportedSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.resolve_supported_source(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/upm-module-api/src/app/mod.rs
Normal file
2
crates/upm-module-api/src/app/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod providers;
|
||||||
|
pub mod search;
|
||||||
42
crates/upm-module-api/src/app/providers.rs
Normal file
42
crates/upm-module-api/src/app/providers.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
use crate::adapters::traits::{AdapterError, AdapterResolution};
|
||||||
|
use crate::app::search::SearchProvider;
|
||||||
|
use crate::domain::source::SourceRef;
|
||||||
|
use crate::domain::update::{ArtifactCandidate, UpdateStrategy};
|
||||||
|
|
||||||
|
pub trait ExternalAddProvider {
|
||||||
|
fn id(&self) -> &'static str;
|
||||||
|
|
||||||
|
fn resolve(&self, source: &SourceRef) -> Result<Option<ExternalAddResolution>, AdapterError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ExternalAddResolution {
|
||||||
|
pub resolution: AdapterResolution,
|
||||||
|
pub selected_artifact: ArtifactCandidate,
|
||||||
|
pub update_strategy: UpdateStrategy,
|
||||||
|
pub display_name_hint: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ProviderRegistry<'a> {
|
||||||
|
pub search_providers: Vec<Box<dyn SearchProvider + 'a>>,
|
||||||
|
pub external_add_providers: Vec<Box<dyn ExternalAddProvider + 'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ProviderRegistry<'a> {
|
||||||
|
pub fn with_search_provider<P>(mut self, provider: P) -> Self
|
||||||
|
where
|
||||||
|
P: SearchProvider + 'a,
|
||||||
|
{
|
||||||
|
self.search_providers.push(Box::new(provider));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_external_add_provider<P>(mut self, provider: P) -> Self
|
||||||
|
where
|
||||||
|
P: ExternalAddProvider + 'a,
|
||||||
|
{
|
||||||
|
self.external_add_providers.push(Box::new(provider));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
23
crates/upm-module-api/src/app/search.rs
Normal file
23
crates/upm-module-api/src/app/search.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
use crate::domain::search::SearchResult;
|
||||||
|
|
||||||
|
pub trait SearchProvider {
|
||||||
|
fn search(
|
||||||
|
&self,
|
||||||
|
query: &crate::domain::search::SearchQuery,
|
||||||
|
) -> Result<Vec<SearchResult>, SearchProviderError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct SearchProviderError {
|
||||||
|
pub provider_id: String,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchProviderError {
|
||||||
|
pub fn new(provider_id: &str, message: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
provider_id: provider_id.to_owned(),
|
||||||
|
message: message.to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
crates/upm-module-api/src/domain/mod.rs
Normal file
3
crates/upm-module-api/src/domain/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod search;
|
||||||
|
pub mod source;
|
||||||
|
pub mod update;
|
||||||
68
crates/upm-module-api/src/domain/search.rs
Normal file
68
crates/upm-module-api/src/domain/search.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
pub const DEFAULT_REMOTE_LIMIT: usize = 10;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum SearchInstallStatus {
|
||||||
|
Available,
|
||||||
|
Installed {
|
||||||
|
installed_version: Option<String>,
|
||||||
|
},
|
||||||
|
UpdateAvailable {
|
||||||
|
installed_version: Option<String>,
|
||||||
|
latest_version: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct SearchQuery {
|
||||||
|
pub text: String,
|
||||||
|
pub remote_limit: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchQuery {
|
||||||
|
pub fn new(text: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.to_owned(),
|
||||||
|
remote_limit: DEFAULT_REMOTE_LIMIT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_remote_limit(text: &str, remote_limit: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.to_owned(),
|
||||||
|
remote_limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct SearchResult {
|
||||||
|
pub provider_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub source_locator: String,
|
||||||
|
pub install_query: String,
|
||||||
|
pub canonical_locator: String,
|
||||||
|
pub version: Option<String>,
|
||||||
|
pub install_status: SearchInstallStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct InstalledSearchMatch {
|
||||||
|
pub stable_id: String,
|
||||||
|
pub display_name: String,
|
||||||
|
pub installed_version: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct SearchWarning {
|
||||||
|
pub provider_id: Option<String>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct SearchResults {
|
||||||
|
pub query_text: String,
|
||||||
|
pub remote_hits: Vec<SearchResult>,
|
||||||
|
pub installed_matches: Vec<InstalledSearchMatch>,
|
||||||
|
pub warnings: Vec<SearchWarning>,
|
||||||
|
}
|
||||||
117
crates/upm-module-api/src/domain/source.rs
Normal file
117
crates/upm-module-api/src/domain/source.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub enum SourceKind {
|
||||||
|
GitHub,
|
||||||
|
GitLab,
|
||||||
|
AppImageHub,
|
||||||
|
SourceForge,
|
||||||
|
DirectUrl,
|
||||||
|
File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceKind {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::GitHub => "github",
|
||||||
|
Self::GitLab => "gitlab",
|
||||||
|
Self::AppImageHub => "appimagehub",
|
||||||
|
Self::SourceForge => "sourceforge",
|
||||||
|
Self::DirectUrl => "direct-url",
|
||||||
|
Self::File => "file",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub enum SourceInputKind {
|
||||||
|
RepoShorthand,
|
||||||
|
GitHubRepositoryUrl,
|
||||||
|
GitHubReleaseUrl,
|
||||||
|
GitHubReleaseAssetUrl,
|
||||||
|
GitLabUrl,
|
||||||
|
AppImageHubUrl,
|
||||||
|
AppImageHubShorthand,
|
||||||
|
SourceForgeUrl,
|
||||||
|
DirectUrl,
|
||||||
|
File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceInputKind {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::RepoShorthand => "repo-shorthand",
|
||||||
|
Self::GitHubRepositoryUrl => "github-repository-url",
|
||||||
|
Self::GitHubReleaseUrl => "github-release-url",
|
||||||
|
Self::GitHubReleaseAssetUrl => "github-release-asset-url",
|
||||||
|
Self::GitLabUrl => "gitlab-url",
|
||||||
|
Self::AppImageHubUrl => "appimagehub-url",
|
||||||
|
Self::AppImageHubShorthand => "appimagehub-shorthand",
|
||||||
|
Self::SourceForgeUrl => "sourceforge-url",
|
||||||
|
Self::DirectUrl => "direct-url",
|
||||||
|
Self::File => "file",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub enum NormalizedSourceKind {
|
||||||
|
GitHubRepository,
|
||||||
|
GitHubRelease,
|
||||||
|
GitHubReleaseAsset,
|
||||||
|
GitLab,
|
||||||
|
GitLabCandidate,
|
||||||
|
AppImageHub,
|
||||||
|
SourceForge,
|
||||||
|
SourceForgeCandidate,
|
||||||
|
DirectUrl,
|
||||||
|
File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NormalizedSourceKind {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::GitHubRepository => "github-repository",
|
||||||
|
Self::GitHubRelease => "github-release",
|
||||||
|
Self::GitHubReleaseAsset => "github-release-asset",
|
||||||
|
Self::GitLab => "gitlab",
|
||||||
|
Self::GitLabCandidate => "gitlab-candidate",
|
||||||
|
Self::AppImageHub => "appimagehub",
|
||||||
|
Self::SourceForge => "sourceforge",
|
||||||
|
Self::SourceForgeCandidate => "sourceforge-candidate",
|
||||||
|
Self::DirectUrl => "direct-url",
|
||||||
|
Self::File => "file",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct SourceRef {
|
||||||
|
pub kind: SourceKind,
|
||||||
|
pub locator: String,
|
||||||
|
#[serde(default = "default_source_input_kind")]
|
||||||
|
pub input_kind: SourceInputKind,
|
||||||
|
#[serde(default = "default_normalized_source_kind")]
|
||||||
|
pub normalized_kind: NormalizedSourceKind,
|
||||||
|
#[serde(default)]
|
||||||
|
pub canonical_locator: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requested_tag: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub requested_asset_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub tracks_latest: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct ResolvedRelease {
|
||||||
|
pub version: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub prerelease: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_source_input_kind() -> SourceInputKind {
|
||||||
|
SourceInputKind::DirectUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn default_normalized_source_kind() -> NormalizedSourceKind {
|
||||||
|
NormalizedSourceKind::DirectUrl
|
||||||
|
}
|
||||||
42
crates/upm-module-api/src/domain/update.rs
Normal file
42
crates/upm-module-api/src/domain/update.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub enum UpdateChannelKind {
|
||||||
|
GitHubReleases,
|
||||||
|
ElectronBuilder,
|
||||||
|
Zsync,
|
||||||
|
DirectAsset,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateChannelKind {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::GitHubReleases => "github-releases",
|
||||||
|
Self::ElectronBuilder => "electron-builder",
|
||||||
|
Self::Zsync => "zsync",
|
||||||
|
Self::DirectAsset => "direct-asset-lineage",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct ChannelPreference {
|
||||||
|
pub kind: UpdateChannelKind,
|
||||||
|
pub locator: String,
|
||||||
|
pub reason: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct UpdateStrategy {
|
||||||
|
pub preferred: ChannelPreference,
|
||||||
|
#[serde(default)]
|
||||||
|
pub alternates: Vec<ChannelPreference>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ArtifactCandidate {
|
||||||
|
pub url: String,
|
||||||
|
pub version: String,
|
||||||
|
pub arch: Option<String>,
|
||||||
|
pub trusted_checksum: Option<String>,
|
||||||
|
pub weak_checksum_md5: Option<String>,
|
||||||
|
pub selection_reason: String,
|
||||||
|
}
|
||||||
3
crates/upm-module-api/src/lib.rs
Normal file
3
crates/upm-module-api/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod adapters;
|
||||||
|
pub mod app;
|
||||||
|
pub mod domain;
|
||||||
|
|
@ -7,19 +7,12 @@ use std::collections::{HashMap, HashSet};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use upm_core::app::add::{
|
use upm_core::app::add::{AddPlan, AddSecurityPolicy, InstalledApp, resolve_requested_scope};
|
||||||
AddPlan, AddSecurityPolicy, InstalledApp,
|
use upm_core::app::list::ListRow;
|
||||||
build_add_plan_with_reporter_and_registered_providers, install_app_with_reporter,
|
|
||||||
resolve_requested_scope,
|
|
||||||
};
|
|
||||||
use upm_core::app::list::{ListRow, build_list_rows};
|
|
||||||
use upm_core::app::progress::{
|
use upm_core::app::progress::{
|
||||||
NoopReporter, OperationEvent, OperationKind, OperationStage, ProgressReporter,
|
NoopReporter, OperationEvent, OperationKind, OperationStage, ProgressReporter,
|
||||||
};
|
};
|
||||||
use upm_core::app::remove::{RemovalResult, remove_registered_app_with_reporter};
|
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::app::AppRecord;
|
||||||
use upm_core::domain::search::{SearchQuery, SearchResults};
|
use upm_core::domain::search::{SearchQuery, SearchResults};
|
||||||
use upm_core::domain::show::{InstalledShow, ShowResult};
|
use upm_core::domain::show::{InstalledShow, ShowResult};
|
||||||
|
|
@ -54,14 +47,15 @@ pub fn dispatch_with_reporter_and_config(
|
||||||
let store = RegistryStore::new(registry_path);
|
let store = RegistryStore::new(registry_path);
|
||||||
let registry = store.load()?;
|
let registry = store.load()?;
|
||||||
let apps = registry.apps.clone();
|
let apps = registry.apps.clone();
|
||||||
|
let app = providers::application();
|
||||||
|
|
||||||
if cli.is_review_update_flow() {
|
if cli.is_review_update_flow() {
|
||||||
return Ok(DispatchResult::UpdatePlan(build_update_plan(&apps)?));
|
return Ok(DispatchResult::UpdatePlan(app.build_update_plan(&apps)?));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(command) = cli.command {
|
if let Some(command) = cli.command {
|
||||||
return match command {
|
return match command {
|
||||||
cli::args::Command::List => Ok(DispatchResult::List(build_list_rows(&apps))),
|
cli::args::Command::List => Ok(DispatchResult::List(app.list(&apps))),
|
||||||
cli::args::Command::Remove { query } => {
|
cli::args::Command::Remove { query } => {
|
||||||
let removal =
|
let removal =
|
||||||
remove_registered_app_with_reporter(&query, &apps, &install_home, reporter)?;
|
remove_registered_app_with_reporter(&query, &apps, &install_home, reporter)?;
|
||||||
|
|
@ -82,13 +76,7 @@ pub fn dispatch_with_reporter_and_config(
|
||||||
kind: OperationKind::Search,
|
kind: OperationKind::Search,
|
||||||
label: query.clone(),
|
label: query.clone(),
|
||||||
});
|
});
|
||||||
let results = providers::with_provider_registry(|providers| {
|
let results = app.search(&SearchQuery::new(&query), &apps)?;
|
||||||
build_search_results_with_registered_providers(
|
|
||||||
&SearchQuery::new(&query),
|
|
||||||
&apps,
|
|
||||||
providers,
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
reporter.report(&OperationEvent::Finished {
|
reporter.report(&OperationEvent::Finished {
|
||||||
summary: format!("search complete: {} remote hits", results.remote_hits.len()),
|
summary: format!("search complete: {} remote hits", results.remote_hits.len()),
|
||||||
});
|
});
|
||||||
|
|
@ -96,13 +84,13 @@ pub fn dispatch_with_reporter_and_config(
|
||||||
}
|
}
|
||||||
cli::args::Command::Show { value } => match value {
|
cli::args::Command::Show { value } => match value {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
let result = build_show_result(&value, &apps)?;
|
let result = app.show(&value, &apps)?;
|
||||||
Ok(DispatchResult::Show(Box::new(result)))
|
Ok(DispatchResult::Show(Box::new(result)))
|
||||||
}
|
}
|
||||||
None => Ok(DispatchResult::ShowAll(build_installed_show_results(&apps))),
|
None => Ok(DispatchResult::ShowAll(app.show_all(&apps))),
|
||||||
},
|
},
|
||||||
cli::args::Command::Update => {
|
cli::args::Command::Update => {
|
||||||
let updates = execute_updates_with_reporter_and_policy(
|
let updates = app.execute_updates(
|
||||||
&apps,
|
&apps,
|
||||||
&install_home,
|
&install_home,
|
||||||
reporter,
|
reporter,
|
||||||
|
|
@ -131,18 +119,13 @@ pub fn dispatch_with_reporter_and_config(
|
||||||
|
|
||||||
if let Some(query) = cli.query {
|
if let Some(query) = cli.query {
|
||||||
let requested_scope = resolve_requested_scope(cli.system, cli.user, is_effective_root());
|
let requested_scope = resolve_requested_scope(cli.system, cli.user, is_effective_root());
|
||||||
let transport = upm_core::source::github::default_transport();
|
let plan_result = app.build_add_plan_with_reporter(
|
||||||
let plan_result = providers::with_provider_registry(|providers| {
|
&query,
|
||||||
build_add_plan_with_reporter_and_registered_providers(
|
reporter,
|
||||||
&query,
|
AddSecurityPolicy {
|
||||||
transport.as_ref(),
|
allow_http_user_sources: config.allow_http,
|
||||||
reporter,
|
},
|
||||||
providers,
|
);
|
||||||
AddSecurityPolicy {
|
|
||||||
allow_http_user_sources: config.allow_http,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let mut plan = match plan_result {
|
let mut plan = match plan_result {
|
||||||
Ok(plan) => plan,
|
Ok(plan) => plan,
|
||||||
Err(
|
Err(
|
||||||
|
|
@ -155,13 +138,7 @@ pub fn dispatch_with_reporter_and_config(
|
||||||
kind: OperationKind::Search,
|
kind: OperationKind::Search,
|
||||||
label: query.clone(),
|
label: query.clone(),
|
||||||
});
|
});
|
||||||
let results = providers::with_provider_registry(|providers| {
|
let results = app.search(&SearchQuery::new(&query), &apps)?;
|
||||||
build_search_results_with_registered_providers(
|
|
||||||
&SearchQuery::new(&query),
|
|
||||||
&apps,
|
|
||||||
providers,
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
reporter.report(&OperationEvent::Finished {
|
reporter.report(&OperationEvent::Finished {
|
||||||
summary: format!("search complete: {} remote hits", results.remote_hits.len()),
|
summary: format!("search complete: {} remote hits", results.remote_hits.len()),
|
||||||
});
|
});
|
||||||
|
|
@ -178,8 +155,7 @@ pub fn dispatch_with_reporter_and_config(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let installed =
|
let installed = app.install_app(&query, &plan, &install_home, requested_scope, reporter)?;
|
||||||
install_app_with_reporter(&query, &plan, &install_home, requested_scope, reporter)?;
|
|
||||||
reporter.report(&OperationEvent::StageChanged {
|
reporter.report(&OperationEvent::StageChanged {
|
||||||
stage: OperationStage::SaveRegistry,
|
stage: OperationStage::SaveRegistry,
|
||||||
message: "saving registry".to_owned(),
|
message: "saving registry".to_owned(),
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,3 @@
|
||||||
use upm_appimage::AppImageHubAddProvider;
|
pub fn application() -> upm_core::UpmApp<'static> {
|
||||||
use upm_appimage::AppImageHubSearchProvider;
|
upm_core::UpmApp::new()
|
||||||
use upm_appimage::source::appimagehub;
|
|
||||||
use upm_core::ProviderRegistry;
|
|
||||||
|
|
||||||
pub fn with_provider_registry<T>(build: impl FnOnce(&ProviderRegistry<'_>) -> T) -> T {
|
|
||||||
let appimagehub_transport = appimagehub::default_transport();
|
|
||||||
let appimagehub_search = AppImageHubSearchProvider::new(appimagehub_transport.as_ref());
|
|
||||||
let appimagehub_add = AppImageHubAddProvider::new(appimagehub_transport.as_ref());
|
|
||||||
let providers = ProviderRegistry {
|
|
||||||
search_providers: vec![&appimagehub_search],
|
|
||||||
external_add_providers: vec![&appimagehub_add],
|
|
||||||
};
|
|
||||||
|
|
||||||
build(&providers)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue