Compare commits

..

No commits in common. "upm" and "main" have entirely different histories.
upm ... main

131 changed files with 961 additions and 3431 deletions

View file

@ -2,92 +2,25 @@
## Workspace Shape ## Workspace Shape
`upm` is a Rust workspace with three main crates today and a fourth planned frontend: `aim` is a Rust workspace with two main crates:
- `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/aim-core`: source normalization, provider adapters, install/update planning, payload installation, registry persistence, and desktop integration.
- `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/aim-cli`: argument parsing, config loading, terminal UX, prompting, progress reporting, and summary rendering.
- `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 intended split is strict: The split keeps product logic in `aim-core` so additional frontends can reuse the same install and update pipeline.
- `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 `aim-cli`.
2. Call the unified application facade in `upm-core`. 2. Resolve the query into a normalized source in `aim-core`.
3. Let `upm-core` route the request into internal orchestration services. 3. Build an add or update plan through provider adapters and artifact selection.
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
@ -100,18 +33,7 @@ Supported source classes currently include:
- direct URLs - direct URLs
- local file imports - local file imports
Core orchestration and normalized module contracts live in `crates/upm-core`. Package-manager-specific behavior belongs in module crates. Provider-specific resolution lives in `crates/aim-core/src/adapters` and `crates/aim-core/src/source`.
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
The rename to `upm` is a hard cutover:
- runtime overrides use `UPM_*`
- legacy `AIM_*` runtime overrides are not read
- default config, registry, payload, and desktop-entry paths use `upm` names
- helper audit logging uses `UPM_DEBUG_EXTERNAL_HELPERS=1`
## Security Hardening State ## Security Hardening State
@ -129,9 +51,9 @@ The remaining deferred AppImageHub host-trust concern is tracked in `security-is
## Persistence And Integration ## Persistence And Integration
- Registry writes are atomic and live under the registry store implementation in `upm-core`. - Registry writes are atomic and live under the registry store implementation in `aim-core`.
- Managed payload, desktop entry, and icon paths are resolved from install policy and scope. - Managed payload, desktop entry, and icon paths are resolved from install policy and scope.
- Desktop integration refresh uses external helpers when available and now supports env-gated audit logging through `UPM_DEBUG_EXTERNAL_HELPERS=1`. - Desktop integration refresh uses external helpers when available and now supports env-gated audit logging through `AIM_DEBUG_EXTERNAL_HELPERS=1`.
## Planning And Audit Artifacts ## Planning And Audit Artifacts

View file

@ -1,214 +0,0 @@
# UPM Roadmap
## Direction
The project is evolving from a focused AppImage manager into `upm`, a modular universal package manager. The target system manages multiple package-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 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.
## Confirmed Product Decisions
- `aim-cli` becomes `upm`.
- `aim-core` stops being the all-in-one backend and is split.
- AppImage support becomes an installable module named `upm-appimage`.
- Shared orchestration, config, state, resolution, ranking, and frontend-facing APIs move into `upm-core`.
- The `upm` crate becomes a thin CLI 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 declarative package file starts in a hybrid mode and is intended to become the source of truth over time.
- Every `upm` invocation should detect drift between declared state and observed system state, then auto-sync metadata as needed.
- Phase 2 is Linux implementation first, with macOS-oriented provider abstractions and packaging seams designed now.
- Feature delivery should happen as vertical slices rather than a single large refactor.
- The `upm` branch is the effective trunk for this evolution work and should be treated as the integration base for future UPM feature branches and worktrees.
## Architectural Destination
### Workspace Shape
The intended workspace shape after the initial refactor is:
- `upm`: thin CLI frontend, ratatui config UI, command routing, presentation.
- `upm-ui`: future GUI frontend over the same application core.
- `upm-core`: headless application layer, public application facade, internal orchestration services, module registry, state model, declarative sync engine, ranking, policies, and frontend-agnostic APIs.
- `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
UPM should stay modular in both code and packaging:
- modules can be enabled or disabled by config
- providers can be ranked by user priority
- distro packaging can offer grouped installs such as `upm-full`
- lighter installs can ship only the core and selected modules
This means 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
The long-term model is declarative and config-first, but Phase 2 begins with a hybrid approach:
- `upm`-managed actions update the declarative config directly
- `upm update` and normal command entrypoints can inspect live system state
- drift detection reconciles unmanaged or changed packages into the config representation
- over time the config becomes authoritative and reconciliation becomes stricter
## Phase 2 Milestones
### Milestone 0: Rename And Split Foundation
Deliver the naming and ownership transition without changing product scope yet.
Goals:
- rename workspace crates and package outputs from `aim` to `upm`
- create `upm-core` by extracting reusable infrastructure from `aim-core`
- reduce the CLI crate to a frontend over headless APIs
- isolate current AppImage-specific logic into `upm-appimage`
- remove direct AppImage composition from the CLI and move module composition into `upm-core`
- preserve current AppImage functionality and tests during the move
Exit criteria:
- `upm` binary replaces `aim`
- workspace builds under new crate names
- AppImage flows still work end-to-end through the new layering
### Milestone 1: AppImage On The New Core
Make the current AppImage implementation the first real package-manager module on the modular architecture.
Goals:
- establish `upm-core` as the sole application boundary for CLI and future GUI frontends
- validate the module contract using AppImage as the reference implementation
- move AppImage-specific acquisition backends behind `upm-appimage`
- prove the CLI can treat AppImage as just one enabled module
Exit criteria:
- AppImage support is no longer special-cased as the whole product
- 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
Add the first non-AppImage providers as Linux-focused vertical slices.
Initial supported sources:
- pacman
- AUR
- Flatpak
- cargo global installs
- npm global installs
Goals:
- implement provider discovery and capability coverage for each source
- normalize package identity, version, installed state, and update candidates across providers
- support search, inspect, install, remove, and update planning where each provider can do so safely
- capture provider limitations explicitly rather than faking uniformity
Exit criteria:
- multi-source search works across enabled providers
- installs and removals work through a consistent command model
- provider-specific metadata is normalized enough for ranking and sync
### Milestone 3: Provider Priority And Config UX
Expose modularity directly in the terminal interface.
Goals:
- build a ratatui configuration menu
- allow enablement and disablement per provider
- allow explicit search and install priority ordering
- allow configuration of module weight, source preference, and future policy toggles
- make search ranking obey configured priority instead of hard-coded source bias
Exit criteria:
- users can manage provider selection and ranking from the TUI
- search results are explainable in terms of configured preference order
### Milestone 4: Declarative Package State And Drift Sync
Introduce the nix-like experience incrementally.
Goals:
- define the declarative package file format in `config.toml` or a closely related managed file
- track installed packages by provider in a normalized state model
- implement `upm update` as both package refresh and state reconciliation
- scan currently installed packages from supported providers and build or refresh the declared package set
- auto-sync detected drift during any `upm` command invocation
Phase 2 intent:
- begin in hybrid mode
- move steadily toward config-first behavior
- avoid destructive reconciliation until provider semantics are trustworthy
Exit criteria:
- users can bootstrap a declarative package definition from the current machine state
- repeated `upm` runs keep declared and observed state aligned
- state drift is surfaced clearly and reconciled predictably
### Milestone 5: Packaging, Distribution, And Platform Seams
Make the modular architecture real at packaging time, not just in code.
Goals:
- define packaging layout for standalone core, selected modules, and full installs
- support distro-level grouped packages such as `upm-full`
- ensure unsupported modules degrade cleanly on the wrong OS or distro
- add macOS provider and packaging seams to `upm-core` even if Linux remains the only implemented provider set in Phase 2
Exit criteria:
- module packaging strategy is documented and testable
- cross-platform abstractions exist without blocking Linux delivery
## Phase 2 Non-Goals
The following are explicitly not required to complete Phase 2:
- full macOS module implementation
- Windows module implementation
- GUI frontend delivery
- forcing strict config-authoritative reconciliation before provider behavior is stable
- shipping every conceivable Linux package manager in the first expansion
## Success Criteria
Phase 2 is successful when the project can credibly be described as a modular package manager rather than an AppImage manager with extra adapters.
That means:
- the product name, workspace shape, and binary identity are all `upm`
- AppImage support is only one module among several
- Linux users can manage packages from the first targeted provider set
- ranking and enablement are user-controlled through config and TUI
- declarative state exists, is importable from the live system, and stays synchronized through normal use
- the architecture is ready for GUI and later platform expansion without another major rewrite
## Immediate Planning Order
Implementation plans should follow this order:
1. rename and crate extraction
2. 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`
4. ratatui configuration and ranking UX
5. declarative state model, drift detection, and `update` sync behavior
6. packaging layout and `upm-full` distribution strategy
This order keeps the refactor defensible, gives each slice a usable product outcome, and avoids locking future provider work into AppImage-era assumptions.

View file

@ -1,410 +0,0 @@
# UPM Rename And Core Extraction Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Rename the product from `aim` to `upm`, remove legacy `aim` runtime interfaces, extract the shared headless backend into `upm-core`, and move AppImage-specific transport and provider logic into a separate `upm-appimage` module without regressing current AppImage workflows.
**Architecture:** Execute this in vertical slices. First rename the workspace, binary, paths, environment interfaces, and tests to `upm` without carrying legacy `aim` compatibility. Next introduce a narrow provider-composition seam in `upm-core` so AppImage-specific add and search logic can move into `upm-appimage` without creating a dependency cycle. Finally rewire the `upm` CLI to assemble built-in providers, update docs, and run full verification.
**Tech Stack:** Rust workspace, Cargo manifests, clap CLI, ratatui frontend crate, core domain/app modules, fixture-backed provider tests, workspace-wide `cargo test` and `cargo clippy`.
---
### Task 1: Rename the workspace, binary, and default runtime paths to `upm`
**Files:**
- Modify: `Cargo.toml`
- Rename: `crates/aim-cli` -> `crates/upm`
- Rename: `crates/aim-core` -> `crates/upm-core`
- Modify: `crates/upm/Cargo.toml`
- Modify: `crates/upm/src/main.rs`
- Modify: `crates/upm/src/lib.rs`
- Modify: `crates/upm/src/cli/args.rs`
- Modify: `crates/upm/src/config.rs`
- Modify: `crates/upm/src/cli/config.rs`
- Modify: `crates/upm-core/Cargo.toml`
- Modify: `crates/upm-core/src/platform/mod.rs`
- Modify: `crates/upm-core/src/integration/paths.rs`
- Modify: `crates/upm-core/src/integration/policy.rs`
- Test: `crates/upm/tests/cli_smoke.rs`
- Test: `crates/upm/tests/cli_commands.rs`
- Test: `crates/upm/tests/config_loading.rs`
- Test: `crates/upm-core/tests/install_paths.rs`
- Test: `crates/upm-core/tests/install_policy.rs`
**Step 1: Write the failing rename expectations**
Update the selected tests to assert:
- the binary name is `upm`
- clap parses `upm` instead of `aim`
- default config path is `~/.config/upm/config.toml`
- default registry path is `~/.local/share/upm/registry.toml`
- default managed payload roots are `.local/lib/upm/appimages` and `/opt/upm/appimages`
- desktop entry filenames use `upm-<stable-id>.desktop`
**Step 2: Run the focused tests to verify failure**
Run:
```bash
cargo test --package aim-cli --test cli_smoke
cargo test --package aim-cli --test config_loading
cargo test --package aim-core --test install_paths
cargo test --package aim-core --test install_policy
```
Expected: FAIL because the workspace still exposes `aim`, `aim-cli`, `aim-core`, and `aim` default paths.
**Step 3: Perform the crate and manifest rename**
Run:
```bash
git mv crates/aim-cli crates/upm
git mv crates/aim-core crates/upm-core
```
Then update:
- workspace members and default members in `Cargo.toml`
- package names to `upm` and `upm-core`
- binary name to `upm`
- crate imports from `aim_core` to `upm_core`
- crate imports from `aim_cli` to `upm`
- clap command name from `aim` to `upm`
- default config, registry, payload-root, and desktop-entry paths to `upm`
**Step 4: Run the focused tests to verify pass**
Run:
```bash
cargo test --package upm --test cli_smoke
cargo test --package upm --test cli_commands
cargo test --package upm --test config_loading
cargo test --package upm-core --test install_paths
cargo test --package upm-core --test install_policy
```
Expected: PASS.
**Step 5: Commit**
```bash
git add Cargo.toml crates/upm crates/upm-core
git commit -m "refactor: rename workspace to upm"
```
### Task 2: Remove remaining `aim`-named runtime interfaces
**Files:**
- Modify: `crates/upm/src/config.rs`
- Modify: `crates/upm/src/cli/config.rs`
- Modify: `crates/upm/src/lib.rs`
- Modify: `crates/upm/src/ui/prompt.rs`
- Modify: `crates/upm-core/src/platform/mod.rs`
- Modify: `crates/upm-core/src/source/github.rs`
- Modify: `crates/upm-core/src/source/appimagehub.rs`
- Modify: `crates/upm-core/src/integration/refresh.rs`
- Test: `crates/upm/tests/config_loading.rs`
**Step 1: Write the failing strict-rename expectations**
Update representative tests to cover:
- config lookup uses `UPM_CONFIG_PATH`
- registry lookup uses `UPM_REGISTRY_PATH`
- old `AIM_*` config and registry overrides are ignored
- tracking preference uses `UPM_TRACKING_PREFERENCE`
- old `AIM_TRACKING_PREFERENCE` is ignored
- provider fixture execution uses the renamed `UPM_*` interfaces through CLI-facing tests
- managed install and summary output use `upm` paths and desktop prefixes
**Step 2: Run the focused tests to verify failure**
Run:
```bash
cargo test --package upm --test config_loading
cargo test --package upm --test search_cli
cargo test --package upm --test end_to_end_cli
cargo test --package upm --test ui_summary
```
Expected: FAIL because representative CLI and config flows still depend on old `aim` names.
**Step 3: Remove the remaining `aim` interfaces**
Update the codebase so renamed runtime interfaces are consistently `upm`:
- environment variable names use `UPM_*`
- helper/debug prefixes print `[upm]`
- GitHub user agent identifies as `upm/0.1`
- old `aim` compatibility reads are removed instead of preserved
**Step 4: Run the focused tests to verify pass**
Run:
```bash
cargo test --package upm --test config_loading
cargo test --package upm --test search_cli
cargo test --package upm --test end_to_end_cli
cargo test --package upm --test ui_summary
```
Expected: PASS.
**Step 5: Commit**
```bash
git add crates/upm/src/config.rs crates/upm/src/cli/config.rs crates/upm/src/lib.rs crates/upm/src/ui/prompt.rs crates/upm-core/src/platform/mod.rs crates/upm-core/src/source/github.rs crates/upm-core/src/source/appimagehub.rs crates/upm-core/src/integration/refresh.rs crates/upm/tests/config_loading.rs
git commit -m "refactor: remove remaining aim runtime interfaces"
```
### Task 3: Add a provider-composition seam in `upm-core`
**Files:**
- Create: `crates/upm-core/src/app/providers.rs`
- Modify: `crates/upm-core/src/app/mod.rs`
- Modify: `crates/upm-core/src/app/add.rs`
- Modify: `crates/upm-core/src/app/search.rs`
- Modify: `crates/upm-core/src/lib.rs`
- Create: `crates/upm-core/tests/provider_registry.rs`
**Step 1: Write the failing provider-composition tests**
Create `crates/upm-core/tests/provider_registry.rs` with two focused tests:
- `build_search_results_with_registered_providers_uses_external_hits` using a stub external search provider
- `build_add_plan_with_registered_providers_delegates_appimagehub_like_sources` using a stub external add provider that returns a fixed artifact and release
The tests should prove that `upm-core` orchestration can consume provider-supplied search and add behavior without hardcoding AppImage-specific modules.
**Step 2: Run the focused tests to verify failure**
Run:
```bash
cargo test --package upm-core --test provider_registry
```
Expected: FAIL because the orchestration layer still hardcodes AppImageHub in `app/add.rs` and `app/search.rs`.
**Step 3: Introduce the narrow composition API**
Create `crates/upm-core/src/app/providers.rs` with minimal types:
- `pub trait ExternalAddProvider`
- `pub struct ExternalAddResolution`
- `pub struct ProviderRegistry<'a>`
Requirements:
- `ProviderRegistry` carries `search_providers: Vec<&'a dyn SearchProvider>` and `external_add_providers: Vec<&'a dyn ExternalAddProvider>`
- `build_search_results` can delegate to `build_search_results_with` using providers supplied by the caller
- `build_add_plan_with_reporter_and_policy` gets a sibling entrypoint that accepts a `ProviderRegistry`
- core built-ins remain in `upm-core`; only AppImage-specific exact-resolution and search logic should move behind the new registry seam
Keep the interface intentionally small. Do not attempt plugin loading or dynamic discovery yet.
**Step 4: Run the focused tests to verify pass**
Run:
```bash
cargo test --package upm-core --test provider_registry
```
Expected: PASS.
**Step 5: Commit**
```bash
git add crates/upm-core/src/app/providers.rs crates/upm-core/src/app/mod.rs crates/upm-core/src/app/add.rs crates/upm-core/src/app/search.rs crates/upm-core/src/lib.rs crates/upm-core/tests/provider_registry.rs
git commit -m "refactor: add provider composition seam to upm-core"
```
### Task 4: Extract AppImage-specific logic into `upm-appimage`
**Files:**
- Modify: `Cargo.toml`
- Create: `crates/upm-appimage/Cargo.toml`
- Create: `crates/upm-appimage/src/lib.rs`
- Create: `crates/upm-appimage/src/add.rs`
- Create: `crates/upm-appimage/src/search.rs`
- Create: `crates/upm-appimage/src/source/mod.rs`
- Create: `crates/upm-appimage/src/source/appimagehub.rs`
- Modify: `crates/upm-core/src/adapters/mod.rs`
- Modify: `crates/upm-core/src/source/mod.rs`
- Modify: `crates/upm-core/src/app/add.rs`
- Modify: `crates/upm-core/src/app/search.rs`
- Create: `crates/upm-appimage/tests/appimagehub_search.rs`
- Modify: `crates/upm-core/tests/adapter_contract.rs`
- Modify: `crates/upm-core/tests/adapter_smoke.rs`
**Step 1: Write the failing extracted-module test**
Create `crates/upm-appimage/tests/appimagehub_search.rs` by moving the current AppImageHub search expectations out of `upm-core` and updating imports to target the new crate.
Also update the affected `upm-core` tests so they no longer import `AppImageHubAdapter` from `upm-core` directly.
**Step 2: Run the focused tests to verify failure**
Run:
```bash
cargo test --package upm-appimage --test appimagehub_search
```
Expected: FAIL because the new crate does not exist yet.
**Step 3: Create the new crate and move AppImageHub implementation into it**
Move the AppImageHub-specific code into the new crate:
- transport and fixture logic from `upm-core/src/source/appimagehub.rs`
- AppImage-backed exact-resolution logic into `crates/upm-appimage/src/add.rs` implementing `ExternalAddProvider`
- AppImageHub search provider logic out of `upm-core/src/app/search.rs` into `crates/upm-appimage/src/search.rs`
`upm-appimage` should depend on `upm-core`, not the other way around.
Leave `SourceKind::AppImageHub`, `SourceInputKind::AppImageHub*`, and `NormalizedSourceKind::AppImageHub` in `upm-core` for this milestone. The deeper provider/domain generalization belongs to the next milestone.
**Step 4: Remove direct AppImageHub wiring from `upm-core`**
Update `upm-core` so it no longer declares:
- `pub mod appimagehub;` in `src/adapters/mod.rs`
- `pub mod appimagehub;` in `src/source/mod.rs`
- built-in AppImageHub search-provider construction in `src/app/search.rs`
- direct `AppImageHubAdapter` imports in `src/app/add.rs`
After this step, AppImage behavior should exist only through the provider registry seam from Task 3.
**Step 5: Run the focused tests to verify pass**
Run:
```bash
cargo test --package upm-appimage --test appimagehub_search
cargo test --package upm-core --test adapter_contract
cargo test --package upm-core --test adapter_smoke
```
Expected: PASS.
**Step 6: Commit**
```bash
git add Cargo.toml crates/upm-appimage crates/upm-core/src/adapters/mod.rs crates/upm-core/src/source/mod.rs crates/upm-core/src/app/add.rs crates/upm-core/src/app/search.rs crates/upm-core/tests/adapter_contract.rs crates/upm-core/tests/adapter_smoke.rs
git commit -m "refactor: extract appimage support into upm-appimage"
```
### Task 5: Rewire the `upm` CLI to assemble built-in providers from modules
**Files:**
- Modify: `crates/upm/Cargo.toml`
- Create: `crates/upm/src/providers.rs`
- Modify: `crates/upm/src/lib.rs`
- Test: `crates/upm/tests/search_cli.rs`
- Test: `crates/upm/tests/end_to_end_cli.rs`
- Test: `crates/upm/tests/ui_summary.rs`
**Step 1: Write the failing CLI integration expectations**
Update CLI integration tests to prove that:
- `upm search firefox` still includes AppImageHub results
- direct `upm appimagehub/2338455` install flow still succeeds through the CLI
- the final summary output still renders the new `upm`-prefixed paths and desktop-entry names
**Step 2: Run the focused tests to verify failure**
Run:
```bash
cargo test --package upm --test search_cli
cargo test --package upm --test end_to_end_cli
cargo test --package upm --test ui_summary
```
Expected: FAIL because `upm` does not yet assemble AppImage providers through the extracted module.
**Step 3: Add CLI-side provider assembly**
Create `crates/upm/src/providers.rs` that:
- builds the `ProviderRegistry` for `upm-core`
- registers the `upm-appimage` search provider
- registers the `upm-appimage` external add provider
Update `crates/upm/src/lib.rs` so dispatch paths call the provider-aware core entrypoints instead of hardcoded core defaults.
Do not move progress rendering or config loading into `upm-core`; the CLI remains the presentation layer.
**Step 4: Run the focused tests to verify pass**
Run:
```bash
cargo test --package upm --test search_cli
cargo test --package upm --test end_to_end_cli
cargo test --package upm --test ui_summary
```
Expected: PASS.
**Step 5: Commit**
```bash
git add crates/upm/Cargo.toml crates/upm/src/providers.rs crates/upm/src/lib.rs crates/upm/tests/search_cli.rs crates/upm/tests/end_to_end_cli.rs crates/upm/tests/ui_summary.rs
git commit -m "refactor: compose providers from upm modules"
```
### Task 6: Update docs and run full workspace verification
**Files:**
- Modify: `README.md`
- Modify: `.architecture/overview.md`
- Modify: `.architecture/roadmap.md`
**Step 1: Update product and architecture docs**
Document:
- the workspace rename to `upm`
- `upm-core` as the headless application layer
- `upm-appimage` as the first installable provider module
- compatibility behavior for existing `aim` config and registry locations
- the fact that provider composition now happens in the CLI rather than through hardcoded AppImage paths in `upm-core`
**Step 2: Verify the docs mention the new structure**
Run:
```bash
rg -n "upm-core|upm-appimage|legacy aim|ProviderRegistry|upm" README.md .architecture/overview.md .architecture/roadmap.md
```
Expected: matches showing the renamed crates, provider split, and compatibility note.
**Step 3: Run the full verification suite**
Run:
```bash
cargo fmt --all
cargo test --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
```
Expected: PASS.
**Step 4: Commit**
```bash
git add README.md .architecture/overview.md .architecture/roadmap.md
git commit -m "docs: describe upm core and module split"
```

View file

@ -1,288 +0,0 @@
# AppImage On The New Core Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**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:** `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.
---
### Task 1: Define the public application facade in `upm-core`
**Files:**
- Modify: `crates/upm-core/src/app/`
- Modify: `crates/upm-core/src/lib.rs`
- Test: `crates/upm-core/tests/`
**Step 1: Write the failing facade expectations**
Add focused tests proving that:
- `upm-core` exposes one public application-facing entrypoint for frontend consumers
- that entrypoint can be constructed without the CLI owning module composition
- the public surface delegates to internal services instead of exposing module wiring details
Keep the assertions about API shape and ownership, not AppImage specifics.
**Step 2: Run the focused tests to verify failure**
Run:
```bash
cargo test --package upm-core
```
Expected: FAIL because the current public surface still assumes narrower provider plumbing and does not expose the intended facade cleanly.
**Step 3: Implement the minimal facade**
Introduce or reshape the public API so `upm-core` exposes a single high-level application facade.
The public boundary should:
- 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
Do not add dynamic plugin loading yet.
**Step 4: Run the focused tests to verify pass**
Run:
```bash
cargo test --package upm-core
```
Expected: PASS.
**Step 5: Commit**
```bash
git add crates/upm-core/src crates/upm-core/tests
git commit -m "feat: add application facade to upm-core"
```
### Task 2: Move module composition out of the CLI and into `upm-core`
**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/update.rs`
- Test: `crates/upm-appimage/tests/`
- Test: `crates/upm-core/tests/`
**Step 1: Write the failing module-boundary expectations**
Add coverage proving that:
- AppImage-backed acquisition through GitHub, GitLab, SourceForge, direct URLs, and AppImageHub resolves through `upm-appimage`
- `upm-core` no longer treats those AppImage-producing backends as top-level package-manager concepts
- add, show, and update continue to work through normalized module contracts
Prefer module-focused tests plus a small number of core integration tests.
**Step 2: Run the focused tests to verify failure**
Run:
```bash
cargo test --package upm-appimage
cargo test --package upm-core
```
Expected: FAIL because AppImage acquisition paths are still split between the core and the AppImage module.
**Step 3: Move AppImage-specific acquisition logic behind the module**
Reshape the source and module boundary so:
- AppImage-specific GitHub, GitLab, SourceForge, AppImageHub, and direct URL handling lives in `upm-appimage`
- `upm-core` coordinates AppImage work through normalized module contracts
- core source taxonomy is reduced or reframed so package-manager concepts stay above backend minutia
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**
Run:
```bash
cargo test --package upm-appimage
cargo test --package upm-core
```
Expected: PASS.
**Step 5: Commit**
```bash
git add crates/upm-appimage/src crates/upm-core/src crates/upm-appimage/tests crates/upm-core/tests
git commit -m "refactor: make upm-appimage the appimage module boundary"
```
### Task 4: Route search, add, show, and update through the application facade
**Files:**
- 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/ui_summary.rs`
- Test: `crates/upm-core/tests/`
**Step 1: Write the failing facade-routing expectations**
Add end-to-end coverage proving that AppImage support is fully module-driven:
- `search` flows through the public application facade
- `add` flows through the public application facade
- `show` flows through the public application facade
- `update` flows through the public application facade
- user-facing summaries still render truthful `upm` paths and origins
Keep the assertions focused on boundary correctness rather than UI restyling.
**Step 2: Run the focused tests to verify failure**
Run:
```bash
cargo test --package upm --test end_to_end_cli
cargo test --package upm --test ui_summary
```
Expected: FAIL until the facade is the normal command path.
**Step 3: Tighten application-boundary validation**
Update the tests so they prove:
- frontend command handlers call the application facade rather than module-specific helpers
- AppImage is composed only through `upm-core` and `upm-appimage`
- AppImage is not reintroduced as a hardcoded built-in in the CLI
Do not reintroduce package-manager-specific branching in the CLI.
**Step 4: Run the focused tests to verify pass**
Run:
```bash
cargo test --package upm --test end_to_end_cli
cargo test --package upm --test ui_summary
```
Expected: PASS.
**Step 5: Commit**
```bash
git add crates/upm-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 "refactor: route commands through upm-core facade"
```
### Task 5: Lock the architecture into docs and verify the workspace
**Files:**
- Modify: `.architecture/overview.md`
- Modify: `.architecture/roadmap.md`
- Modify: `README.md`
**Step 1: Update docs for the new module model**
Document:
- `upm-core` as the application boundary
- one public application facade over smaller internal services
- CLI and GUI as thin frontends
- `upm-appimage` as the AppImage package-manager boundary with internal acquisition backends
**Step 2: Verify the docs mention the agreed architecture**
Run:
```bash
rg -n "upm-core|facade|upm-ui|upm-appimage|module" README.md .architecture/overview.md .architecture/roadmap.md
```
Expected: matches describing the application boundary, thin frontends, and module ownership.
**Step 3: Run full verification**
Run:
```bash
cargo fmt --all
cargo test --workspace
cargo clippy --workspace --all-targets --all-features -- -D warnings
```
Expected: PASS.
**Step 4: Commit**
```bash
git add README.md .architecture/overview.md .architecture/roadmap.md
git commit -m "docs: describe application facade architecture"
```

View file

@ -11,7 +11,3 @@ IMPORTANT TO CHECK BEFORE ANY COMMIT!!
Architecture under `.architecture` must be maintained with each change. Architecture under `.architecture` must be maintained with each change.
- Security issues stumbled upon or noticed during execution **already in code** must live in `security-issues.md`. Newly added issues during execution or planning should be raised to the user and/or dealt with, instead of growing the list. - Security issues stumbled upon or noticed during execution **already in code** must live in `security-issues.md`. Newly added issues during execution or planning should be raised to the user and/or dealt with, instead of growing the list.
- An overview of the workspace, should live in `overview.md`. - An overview of the workspace, should live in `overview.md`.
- A roadmap of the planned direction & features in `roadmap.md`.
## UPM work
We are currently using the `upm` branch as our "main" for all `upm` work, meaning feature branches/worktrees will merge back into here.

91
Cargo.lock generated
View file

@ -11,6 +11,41 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "aim-cli"
version = "0.1.0"
dependencies = [
"aim-core",
"assert_cmd",
"clap",
"console 0.16.3",
"crossterm",
"dialoguer",
"indicatif",
"libc",
"predicates",
"ratatui",
"serde",
"tempfile",
"toml",
]
[[package]]
name = "aim-core"
version = "0.1.0"
dependencies = [
"base64",
"fs2",
"md5",
"quick-xml",
"reqwest",
"serde",
"serde_yaml",
"sha2",
"tempfile",
"toml",
]
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.21" version = "0.2.21"
@ -1931,62 +1966,6 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "upm"
version = "0.1.0"
dependencies = [
"assert_cmd",
"clap",
"console 0.16.3",
"crossterm",
"dialoguer",
"indicatif",
"libc",
"predicates",
"ratatui",
"serde",
"tempfile",
"toml",
"upm-appimage",
"upm-core",
]
[[package]]
name = "upm-appimage"
version = "0.1.0"
dependencies = [
"quick-xml",
"reqwest",
"serde",
"upm-core",
"upm-module-api",
]
[[package]]
name = "upm-core"
version = "0.1.0"
dependencies = [
"base64",
"fs2",
"md5",
"quick-xml",
"reqwest",
"serde",
"serde_yaml",
"sha2",
"tempfile",
"toml",
"upm-appimage",
"upm-module-api",
]
[[package]]
name = "upm-module-api"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.8" version = "2.5.8"

View file

@ -1,12 +1,10 @@
[workspace] [workspace]
members = [ members = [
"crates/upm-module-api", "crates/aim-core",
"crates/upm-core", "crates/aim-cli",
"crates/upm-appimage",
"crates/upm",
] ]
default-members = [ default-members = [
"crates/upm", "crates/aim-cli",
] ]
resolver = "2" resolver = "2"

View file

@ -1,26 +1,24 @@
# upm # aim
Universal Package Manager AppImage Manager
`upm` is a Rust Cargo workspace for a modular package manager with a shared headless application core, thin frontend crates, and package-manager modules. `aim` is a Rust Cargo workspace for managing AppImages from multiple source types.
## Workspace ## Workspace
- `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/aim-core`: business logic, source adapters, registry, install/update planning
- `crates/upm`: thin terminal frontend for argument parsing, config loading, prompting, progress reporting, and summary rendering - `crates/aim-cli`: thin terminal frontend for parsing, prompting, and rendering
- `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-manager behavior stays modular instead of being hardcoded into the core or the CLI. The split is intentional so a future GUI client can reuse `aim-core` without moving logic out of the shared library.
## Commands ## Commands
```text ```text
upm <QUERY> aim <QUERY>
upm aim
upm update aim update
upm list aim list
upm search <QUERY> aim search <QUERY>
upm remove <QUERY> aim remove <QUERY>
``` ```
## Query Forms ## Query Forms
@ -38,22 +36,22 @@ upm remove <QUERY>
## Search ## Search
`upm search <QUERY>` is part of the initial modular module surface. `aim search <QUERY>` is part of v0.9 finalisation.
- search is module-extensible and currently includes the built-in core search path plus AppImage-backed search sources - search is provider-extensible and currently includes GitHub plus AppImageHub
- 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>`
- module composition belongs in `upm-core`, not in the CLI frontend - the search model is provider-extensible for future phases
## Scope Overrides ## Scope Overrides
By default `upm` auto-detects whether to use user or system scope. Override that with: By default `aim` auto-detects whether to use user or system scope. Override that with:
- `--user` - `--user`
- `--system` - `--system`
## Config ## Config
Runtime config is loaded from `~/.config/upm/config.toml` or `$XDG_CONFIG_HOME/upm/config.toml`. Runtime config is loaded from `~/.config/aim/config.toml` or `$XDG_CONFIG_HOME/aim/config.toml`.
Example: Example:
@ -65,20 +63,13 @@ allow_http = true
- `allow_http` only permits user-supplied `http://` inputs such as direct URL installs or updates from previously installed direct HTTP origins - `allow_http` only permits user-supplied `http://` inputs such as direct URL installs or updates from previously installed direct HTTP origins
- provider-resolved downloads such as AppImageHub artifacts remain HTTPS-only even when `allow_http = true` - provider-resolved downloads such as AppImageHub artifacts remain HTTPS-only even when `allow_http = true`
## Breaking Rename
- `upm` is a hard rename from `aim`
- runtime overrides now use `UPM_*` names such as `UPM_CONFIG_PATH` and `UPM_REGISTRY_PATH`
- old `AIM_*` runtime overrides are intentionally ignored
- default config and registry locations now live under `upm` paths
## Current Flow Shape ## Current Flow Shape
- `upm <QUERY>` installs direct provider matches when available, otherwise falls back to search results, shows live progress on stderr, prints an `Installation Summary` on stdout for installs, and renders an `Installation Review` when tracking needs confirmation - `aim <QUERY>` installs direct provider matches when available, otherwise falls back to search results, shows live progress on stderr, prints an `Installation Summary` on stdout for installs, and renders an `Installation Review` when tracking needs confirmation
- bare `upm` prints an `Update Review` without mutating the registry - bare `aim` prints an `Update Review` without mutating the registry
- `upm update` executes the pending updates, streams live status on stderr, then prints an `Update Summary` - `aim update` executes the pending updates, streams live status on stderr, then prints an `Update Summary`
- `upm list` renders either `Installed Apps` or `No installed apps yet` - `aim list` renders either `Installed Apps` or `No installed apps yet`
- `upm remove <QUERY>` resolves a registered application name, streams removal progress on stderr, then prints a `Removal Summary` - `aim remove <QUERY>` resolves a registered application name, streams removal progress on stderr, then prints a `Removal Summary`
## Terminal UX ## Terminal UX

View file

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

View file

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

View file

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

View file

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

View file

@ -1,23 +1,28 @@
pub mod cli; pub mod cli;
pub mod config; pub mod config;
pub mod providers;
pub mod ui; pub mod ui;
use std::collections::{HashMap, HashSet}; 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::{AddPlan, AddSecurityPolicy, InstalledApp, resolve_requested_scope}; use aim_core::app::add::{
use upm_core::app::list::ListRow; AddPlan, AddSecurityPolicy, InstalledApp, build_add_plan_with_reporter_and_policy,
use upm_core::app::progress::{ install_app_with_reporter, resolve_requested_scope,
};
use aim_core::app::list::{ListRow, build_list_rows};
use aim_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 aim_core::app::remove::{RemovalResult, remove_registered_app_with_reporter};
use upm_core::domain::app::AppRecord; use aim_core::app::search::build_search_results;
use upm_core::domain::search::{SearchQuery, SearchResults}; use aim_core::app::show::{build_installed_show_results, build_show_result};
use upm_core::domain::show::{InstalledShow, ShowResult}; use aim_core::app::update::{build_update_plan, execute_updates_with_reporter_and_policy};
use upm_core::domain::update::{UpdateExecutionResult, UpdatePlan}; use aim_core::domain::app::AppRecord;
use upm_core::registry::store::RegistryStore; use aim_core::domain::search::{SearchQuery, SearchResults};
use aim_core::domain::show::{InstalledShow, ShowResult};
use aim_core::domain::update::{UpdateExecutionResult, UpdatePlan};
use aim_core::registry::store::RegistryStore;
pub use cli::args::Cli; pub use cli::args::Cli;
@ -47,15 +52,14 @@ 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(app.build_update_plan(&apps)?)); return Ok(DispatchResult::UpdatePlan(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(app.list(&apps))), cli::args::Command::List => Ok(DispatchResult::List(build_list_rows(&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)?;
@ -76,7 +80,7 @@ pub fn dispatch_with_reporter_and_config(
kind: OperationKind::Search, kind: OperationKind::Search,
label: query.clone(), label: query.clone(),
}); });
let results = app.search(&SearchQuery::new(&query), &apps)?; let results = build_search_results(&SearchQuery::new(&query), &apps)?;
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()),
}); });
@ -84,13 +88,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 = app.show(&value, &apps)?; let result = build_show_result(&value, &apps)?;
Ok(DispatchResult::Show(Box::new(result))) Ok(DispatchResult::Show(Box::new(result)))
} }
None => Ok(DispatchResult::ShowAll(app.show_all(&apps))), None => Ok(DispatchResult::ShowAll(build_installed_show_results(&apps))),
}, },
cli::args::Command::Update => { cli::args::Command::Update => {
let updates = app.execute_updates( let updates = execute_updates_with_reporter_and_policy(
&apps, &apps,
&install_home, &install_home,
reporter, reporter,
@ -119,8 +123,10 @@ 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 plan_result = app.build_add_plan_with_reporter( let transport = aim_core::source::github::default_transport();
let plan_result = build_add_plan_with_reporter_and_policy(
&query, &query,
transport.as_ref(),
reporter, reporter,
AddSecurityPolicy { AddSecurityPolicy {
allow_http_user_sources: config.allow_http, allow_http_user_sources: config.allow_http,
@ -129,16 +135,16 @@ pub fn dispatch_with_reporter_and_config(
let mut plan = match plan_result { let mut plan = match plan_result {
Ok(plan) => plan, Ok(plan) => plan,
Err( Err(
upm_core::app::add::BuildAddPlanError::Query( aim_core::app::add::BuildAddPlanError::Query(
upm_core::app::query::ResolveQueryError::Unsupported, aim_core::app::query::ResolveQueryError::Unsupported,
) )
| upm_core::app::add::BuildAddPlanError::NoInstallableArtifact { .. }, | aim_core::app::add::BuildAddPlanError::NoInstallableArtifact { .. },
) => { ) => {
reporter.report(&OperationEvent::Started { reporter.report(&OperationEvent::Started {
kind: OperationKind::Search, kind: OperationKind::Search,
label: query.clone(), label: query.clone(),
}); });
let results = app.search(&SearchQuery::new(&query), &apps)?; let results = build_search_results(&SearchQuery::new(&query), &apps)?;
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()),
}); });
@ -155,7 +161,8 @@ pub fn dispatch_with_reporter_and_config(
} }
} }
let installed = app.install_app(&query, &plan, &install_home, requested_scope, reporter)?; let installed =
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(),
@ -181,17 +188,13 @@ pub fn render_with_config(result: &DispatchResult, config: &config::CliConfig) -
ui::render::render_dispatch_result_with_config(result, config) ui::render::render_dispatch_result_with_config(result, config)
} }
pub fn default_registry_path() -> PathBuf { fn registry_path() -> PathBuf {
if let Some(path) = env::var_os("UPM_REGISTRY_PATH") { if let Some(path) = env::var_os("AIM_REGISTRY_PATH") {
return PathBuf::from(path); return PathBuf::from(path);
} }
let home = env::var_os("HOME").unwrap_or_else(|| ".".into()); let home = env::var_os("HOME").unwrap_or_else(|| ".".into());
PathBuf::from(home).join(".local/share/upm/registry.toml") PathBuf::from(home).join(".local/share/aim/registry.toml")
}
fn registry_path() -> PathBuf {
default_registry_path()
} }
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
@ -210,49 +213,49 @@ pub enum DispatchResult {
#[derive(Debug)] #[derive(Debug)]
pub enum DispatchError { pub enum DispatchError {
AddPlan(upm_core::app::add::BuildAddPlanError), AddPlan(aim_core::app::add::BuildAddPlanError),
AddInstall(upm_core::app::add::InstallAppError), AddInstall(aim_core::app::add::InstallAppError),
Prompt(ui::prompt::PromptError), Prompt(ui::prompt::PromptError),
RemovePlan(upm_core::app::remove::RemoveRegisteredAppError), RemovePlan(aim_core::app::remove::RemoveRegisteredAppError),
Registry(upm_core::registry::store::RegistryStoreError), Registry(aim_core::registry::store::RegistryStoreError),
Search(upm_core::app::search::SearchError), Search(aim_core::app::search::SearchError),
Show(upm_core::domain::show::ShowResultError), Show(aim_core::domain::show::ShowResultError),
UpdatePlan(upm_core::app::update::BuildUpdatePlanError), UpdatePlan(aim_core::app::update::BuildUpdatePlanError),
UpdateExecution(upm_core::app::update::ExecuteUpdatesError), UpdateExecution(aim_core::app::update::ExecuteUpdatesError),
} }
impl std::fmt::Display for DispatchError { impl std::fmt::Display for DispatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::AddPlan(error) => match error { Self::AddPlan(error) => match error {
upm_core::app::add::BuildAddPlanError::Query( aim_core::app::add::BuildAddPlanError::Query(
upm_core::app::query::ResolveQueryError::Unsupported, aim_core::app::query::ResolveQueryError::Unsupported,
) => write!(f, "unsupported source query"), ) => write!(f, "unsupported source query"),
upm_core::app::add::BuildAddPlanError::InsecureHttpSource { .. } => write!( aim_core::app::add::BuildAddPlanError::InsecureHttpSource { .. } => write!(
f, f,
"insecure HTTP sources are disabled; set allow_http = true to permit them" "insecure HTTP sources are disabled; set allow_http = true to permit them"
), ),
upm_core::app::add::BuildAddPlanError::NoInstallableArtifact { source } => write!( aim_core::app::add::BuildAddPlanError::NoInstallableArtifact { source } => write!(
f, f,
"no installable artifact found for {} {}", "no installable artifact found for {} {}",
source.kind.as_str(), source.kind.as_str(),
source.locator source.locator
), ),
upm_core::app::add::BuildAddPlanError::Adapter(id, error) => match error { aim_core::app::add::BuildAddPlanError::Adapter(id, error) => match error {
upm_core::adapters::traits::AdapterError::UnsupportedQuery => { aim_core::adapters::traits::AdapterError::UnsupportedQuery => {
write!(f, "{id} does not support this query") write!(f, "{id} does not support this query")
} }
upm_core::adapters::traits::AdapterError::UnsupportedSource => { aim_core::adapters::traits::AdapterError::UnsupportedSource => {
write!(f, "{id} does not support this source") write!(f, "{id} does not support this source")
} }
upm_core::adapters::traits::AdapterError::ResolutionFailed(reason) => { aim_core::adapters::traits::AdapterError::ResolutionFailed(reason) => {
write!(f, "{id} resolution failed: {reason}") write!(f, "{id} resolution failed: {reason}")
} }
}, },
upm_core::app::add::BuildAddPlanError::GitHubDiscovery(error) => { aim_core::app::add::BuildAddPlanError::GitHubDiscovery(error) => {
write!(f, "github discovery failed: {error:?}") write!(f, "github discovery failed: {error:?}")
} }
upm_core::app::add::BuildAddPlanError::NoCandidates => { aim_core::app::add::BuildAddPlanError::NoCandidates => {
write!(f, "no installable candidates found") write!(f, "no installable candidates found")
} }
}, },
@ -262,7 +265,7 @@ impl std::fmt::Display for DispatchError {
Self::Registry(error) => write!(f, "registry failed: {error:?}"), Self::Registry(error) => write!(f, "registry failed: {error:?}"),
Self::Search(error) => write!(f, "search failed: {error:?}"), Self::Search(error) => write!(f, "search failed: {error:?}"),
Self::Show(error) => match error { Self::Show(error) => match error {
upm_core::domain::show::ShowResultError::AmbiguousInstalledMatch { aim_core::domain::show::ShowResultError::AmbiguousInstalledMatch {
query, query,
matches, matches,
} => write!( } => write!(
@ -270,14 +273,14 @@ impl std::fmt::Display for DispatchError {
"multiple installed apps match {query}: {}", "multiple installed apps match {query}: {}",
matches.join(", ") matches.join(", ")
), ),
upm_core::domain::show::ShowResultError::UnsupportedQuery => { aim_core::domain::show::ShowResultError::UnsupportedQuery => {
write!(f, "unsupported source query") write!(f, "unsupported source query")
} }
upm_core::domain::show::ShowResultError::InsecureHttpSource => write!( aim_core::domain::show::ShowResultError::InsecureHttpSource => write!(
f, f,
"insecure HTTP sources are disabled; set allow_http = true to permit them" "insecure HTTP sources are disabled; set allow_http = true to permit them"
), ),
upm_core::domain::show::ShowResultError::NoInstallableArtifact { source } => { aim_core::domain::show::ShowResultError::NoInstallableArtifact { source } => {
write!( write!(
f, f,
"no installable artifact found for {} {}", "no installable artifact found for {} {}",
@ -285,18 +288,18 @@ impl std::fmt::Display for DispatchError {
source.locator source.locator
) )
} }
upm_core::domain::show::ShowResultError::AdapterResolutionFailed { aim_core::domain::show::ShowResultError::AdapterResolutionFailed {
adapter_id, adapter_id,
kind, kind,
detail, detail,
} => match kind { } => match kind {
upm_core::domain::show::AdapterFailureKind::UnsupportedQuery => { aim_core::domain::show::AdapterFailureKind::UnsupportedQuery => {
write!(f, "{adapter_id} does not support this query") write!(f, "{adapter_id} does not support this query")
} }
upm_core::domain::show::AdapterFailureKind::UnsupportedSource => { aim_core::domain::show::AdapterFailureKind::UnsupportedSource => {
write!(f, "{adapter_id} does not support this source") write!(f, "{adapter_id} does not support this source")
} }
upm_core::domain::show::AdapterFailureKind::ResolutionFailed => { aim_core::domain::show::AdapterFailureKind::ResolutionFailed => {
if let Some(detail) = detail { if let Some(detail) = detail {
write!(f, "{adapter_id} resolution failed: {detail}") write!(f, "{adapter_id} resolution failed: {detail}")
} else { } else {
@ -304,27 +307,27 @@ impl std::fmt::Display for DispatchError {
} }
} }
}, },
upm_core::domain::show::ShowResultError::GitHubDiscoveryFailed { aim_core::domain::show::ShowResultError::GitHubDiscoveryFailed {
kind, kind,
detail, detail,
} => match (kind, detail) { } => match (kind, detail) {
( (
upm_core::domain::show::GitHubDiscoveryFailureKind::FixtureDocumentMissing, aim_core::domain::show::GitHubDiscoveryFailureKind::FixtureDocumentMissing,
Some(detail), Some(detail),
) => write!(f, "github discovery failed: missing fixture document {detail}"), ) => write!(f, "github discovery failed: missing fixture document {detail}"),
( (
upm_core::domain::show::GitHubDiscoveryFailureKind::NoReleases, aim_core::domain::show::GitHubDiscoveryFailureKind::NoReleases,
Some(detail), Some(detail),
) => write!(f, "github discovery failed: no releases for {detail}"), ) => write!(f, "github discovery failed: no releases for {detail}"),
(upm_core::domain::show::GitHubDiscoveryFailureKind::Unsupported, _) => { (aim_core::domain::show::GitHubDiscoveryFailureKind::Unsupported, _) => {
write!(f, "github discovery failed: unsupported source") write!(f, "github discovery failed: unsupported source")
} }
(upm_core::domain::show::GitHubDiscoveryFailureKind::Transport, _) => { (aim_core::domain::show::GitHubDiscoveryFailureKind::Transport, _) => {
write!(f, "github discovery failed: transport error") write!(f, "github discovery failed: transport error")
} }
_ => write!(f, "github discovery failed"), _ => write!(f, "github discovery failed"),
}, },
upm_core::domain::show::ShowResultError::NoInstallableCandidates => { aim_core::domain::show::ShowResultError::NoInstallableCandidates => {
write!(f, "no installable candidates found") write!(f, "no installable candidates found")
} }
}, },
@ -334,25 +337,25 @@ impl std::fmt::Display for DispatchError {
} }
} }
fn render_install_error(error: &upm_core::app::add::InstallAppError) -> String { fn render_install_error(error: &aim_core::app::add::InstallAppError) -> String {
match error { match error {
upm_core::app::add::InstallAppError::Materialize(error) => format!("{error:?}"), aim_core::app::add::InstallAppError::Materialize(error) => format!("{error:?}"),
upm_core::app::add::InstallAppError::Policy(error) => error.clone(), aim_core::app::add::InstallAppError::Policy(error) => error.clone(),
upm_core::app::add::InstallAppError::Download(error) => error.to_string(), aim_core::app::add::InstallAppError::Download(error) => error.to_string(),
upm_core::app::add::InstallAppError::DownloadIo(error) => error.to_string(), aim_core::app::add::InstallAppError::DownloadIo(error) => error.to_string(),
upm_core::app::add::InstallAppError::HostProbe(error) => error.to_string(), aim_core::app::add::InstallAppError::HostProbe(error) => error.to_string(),
upm_core::app::add::InstallAppError::Install(error) => error.to_string(), aim_core::app::add::InstallAppError::Install(error) => error.to_string(),
} }
} }
impl From<upm_core::app::add::BuildAddPlanError> for DispatchError { impl From<aim_core::app::add::BuildAddPlanError> for DispatchError {
fn from(value: upm_core::app::add::BuildAddPlanError) -> Self { fn from(value: aim_core::app::add::BuildAddPlanError) -> Self {
Self::AddPlan(value) Self::AddPlan(value)
} }
} }
impl From<upm_core::app::add::InstallAppError> for DispatchError { impl From<aim_core::app::add::InstallAppError> for DispatchError {
fn from(value: upm_core::app::add::InstallAppError) -> Self { fn from(value: aim_core::app::add::InstallAppError) -> Self {
Self::AddInstall(value) Self::AddInstall(value)
} }
} }
@ -363,38 +366,38 @@ impl From<ui::prompt::PromptError> for DispatchError {
} }
} }
impl From<upm_core::app::update::BuildUpdatePlanError> for DispatchError { impl From<aim_core::app::update::BuildUpdatePlanError> for DispatchError {
fn from(value: upm_core::app::update::BuildUpdatePlanError) -> Self { fn from(value: aim_core::app::update::BuildUpdatePlanError) -> Self {
Self::UpdatePlan(value) Self::UpdatePlan(value)
} }
} }
impl From<upm_core::app::update::ExecuteUpdatesError> for DispatchError { impl From<aim_core::app::update::ExecuteUpdatesError> for DispatchError {
fn from(value: upm_core::app::update::ExecuteUpdatesError) -> Self { fn from(value: aim_core::app::update::ExecuteUpdatesError) -> Self {
Self::UpdateExecution(value) Self::UpdateExecution(value)
} }
} }
impl From<upm_core::app::remove::RemoveRegisteredAppError> for DispatchError { impl From<aim_core::app::remove::RemoveRegisteredAppError> for DispatchError {
fn from(value: upm_core::app::remove::RemoveRegisteredAppError) -> Self { fn from(value: aim_core::app::remove::RemoveRegisteredAppError) -> Self {
Self::RemovePlan(value) Self::RemovePlan(value)
} }
} }
impl From<upm_core::registry::store::RegistryStoreError> for DispatchError { impl From<aim_core::registry::store::RegistryStoreError> for DispatchError {
fn from(value: upm_core::registry::store::RegistryStoreError) -> Self { fn from(value: aim_core::registry::store::RegistryStoreError) -> Self {
Self::Registry(value) Self::Registry(value)
} }
} }
impl From<upm_core::app::search::SearchError> for DispatchError { impl From<aim_core::app::search::SearchError> for DispatchError {
fn from(value: upm_core::app::search::SearchError) -> Self { fn from(value: aim_core::app::search::SearchError) -> Self {
Self::Search(value) Self::Search(value)
} }
} }
impl From<upm_core::domain::show::ShowResultError> for DispatchError { impl From<aim_core::domain::show::ShowResultError> for DispatchError {
fn from(value: upm_core::domain::show::ShowResultError) -> Self { fn from(value: aim_core::domain::show::ShowResultError) -> Self {
Self::Show(value) Self::Show(value)
} }
} }
@ -439,7 +442,7 @@ fn merge_updated_app_records(
} }
fn install_home(registry_path: &Path) -> PathBuf { fn install_home(registry_path: &Path) -> PathBuf {
if env::var_os("UPM_REGISTRY_PATH").is_some() { if env::var_os("AIM_REGISTRY_PATH").is_some() {
return registry_path return registry_path
.parent() .parent()
.unwrap_or_else(|| Path::new(".")) .unwrap_or_else(|| Path::new("."))
@ -451,7 +454,7 @@ fn install_home(registry_path: &Path) -> PathBuf {
} }
fn is_effective_root() -> bool { fn is_effective_root() -> bool {
if let Some(value) = env::var_os("UPM_EFFECTIVE_ROOT") { if let Some(value) = env::var_os("AIM_EFFECTIVE_ROOT") {
let value = value.to_string_lossy(); let value = value.to_string_lossy();
return value == "1" || value.eq_ignore_ascii_case("true"); return value == "1" || value.eq_ignore_ascii_case("true");
} }

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
use console::measure_text_width; use aim_core::app::add::AddPlan;
use upm_core::app::add::AddPlan; use aim_core::domain::search::SearchResults;
use upm_core::domain::search::SearchResults; use aim_core::domain::show::{
use upm_core::domain::show::{
InstalledShow, MetadataSummary, RemoteInteractionSummary, RemoteShow, ShowResult, SourceSummary, InstalledShow, MetadataSummary, RemoteInteractionSummary, RemoteShow, ShowResult, SourceSummary,
}; };
use upm_core::domain::update::UpdateExecutionStatus; use aim_core::domain::update::UpdateExecutionStatus;
use console::measure_text_width;
use crate::DispatchResult; use crate::DispatchResult;
use crate::config::CliConfig; use crate::config::CliConfig;
@ -38,10 +38,10 @@ pub fn render_dispatch_result_with_config(result: &DispatchResult, config: &CliC
} }
} }
fn render_added_app(added: &upm_core::app::add::InstalledApp) -> String { fn render_added_app(added: &aim_core::app::add::InstalledApp) -> String {
let scope = match added.install_scope { let scope = match added.install_scope {
upm_core::domain::app::InstallScope::User => "user", aim_core::domain::app::InstallScope::User => "user",
upm_core::domain::app::InstallScope::System => "system", aim_core::domain::app::InstallScope::System => "system",
}; };
let warning_lines = added let warning_lines = added
@ -104,7 +104,7 @@ fn render_pending_add(plan: &AddPlan) -> String {
.join("\n") .join("\n")
} }
fn render_list(rows: &[upm_core::app::list::ListRow]) -> String { fn render_list(rows: &[aim_core::app::list::ListRow]) -> String {
if rows.is_empty() { if rows.is_empty() {
return crate::ui::theme::muted("No installed apps yet"); return crate::ui::theme::muted("No installed apps yet");
} }
@ -170,7 +170,7 @@ fn format_list_row(
} }
} }
fn render_removed_app(removed: &upm_core::app::remove::RemovalResult) -> String { fn render_removed_app(removed: &aim_core::app::remove::RemovalResult) -> String {
let warning_lines = removed let warning_lines = removed
.warnings .warnings
.iter() .iter()
@ -385,10 +385,10 @@ fn metadata_detail_lines(metadata: &MetadataSummary) -> Vec<String> {
lines lines
} }
fn installed_files_header(scope: Option<upm_core::domain::app::InstallScope>) -> String { fn installed_files_header(scope: Option<aim_core::domain::app::InstallScope>) -> String {
let label = match scope { let label = match scope {
Some(upm_core::domain::app::InstallScope::User) => "Installed as User", Some(aim_core::domain::app::InstallScope::User) => "Installed as User",
Some(upm_core::domain::app::InstallScope::System) => "Installed as System", Some(aim_core::domain::app::InstallScope::System) => "Installed as System",
None => "Installed files", None => "Installed files",
}; };
@ -422,11 +422,11 @@ fn truncate_checksum(checksum: &str) -> String {
} }
} }
fn metadata_kind_label(kind: upm_core::domain::update::ParsedMetadataKind) -> &'static str { fn metadata_kind_label(kind: aim_core::domain::update::ParsedMetadataKind) -> &'static str {
match kind { match kind {
upm_core::domain::update::ParsedMetadataKind::Unknown => "unknown", aim_core::domain::update::ParsedMetadataKind::Unknown => "unknown",
upm_core::domain::update::ParsedMetadataKind::ElectronBuilder => "electron-builder", aim_core::domain::update::ParsedMetadataKind::ElectronBuilder => "electron-builder",
upm_core::domain::update::ParsedMetadataKind::Zsync => "zsync", aim_core::domain::update::ParsedMetadataKind::Zsync => "zsync",
} }
} }
@ -509,7 +509,7 @@ fn render_remote_show(remote: &RemoteShow) -> String {
lines.join("\n") lines.join("\n")
} }
fn install_file_paths(added: &upm_core::app::add::InstalledApp) -> Vec<String> { fn install_file_paths(added: &aim_core::app::add::InstalledApp) -> Vec<String> {
[ [
Some( Some(
added added
@ -595,7 +595,7 @@ fn render_search_results_with_config(results: &SearchResults, config: &CliConfig
render_search_results(results) render_search_results(results)
} }
fn render_updated_apps(result: &upm_core::domain::update::UpdateExecutionResult) -> String { fn render_updated_apps(result: &aim_core::domain::update::UpdateExecutionResult) -> String {
let mut lines = vec![ let mut lines = vec![
crate::ui::theme::heading("Update Summary"), crate::ui::theme::heading("Update Summary"),
format!("updated apps: {}", result.updated_count()), format!("updated apps: {}", result.updated_count()),
@ -621,7 +621,7 @@ fn render_updated_apps(result: &upm_core::domain::update::UpdateExecutionResult)
lines.join("\n") lines.join("\n")
} }
fn render_update_plan(plan: &upm_core::domain::update::UpdatePlan) -> String { fn render_update_plan(plan: &aim_core::domain::update::UpdatePlan) -> String {
let mut lines = vec![render_update_summary(plan.items.len(), plan.items.len(), 0)]; let mut lines = vec![render_update_summary(plan.items.len(), plan.items.len(), 0)];
for item in &plan.items { for item in &plan.items {

View file

@ -2,6 +2,7 @@ use std::collections::BTreeSet;
use std::io::IsTerminal; use std::io::IsTerminal;
use std::time::Duration; use std::time::Duration;
use aim_core::domain::search::{SearchInstallStatus, SearchResult, SearchResults};
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use crossterm::execute; use crossterm::execute;
use crossterm::terminal::{ use crossterm::terminal::{
@ -13,7 +14,6 @@ use ratatui::style::Modifier;
use ratatui::text::{Line, Span}; use ratatui::text::{Line, Span};
use ratatui::widgets::{Clear, List, ListItem, Paragraph, Wrap}; use ratatui::widgets::{Clear, List, ListItem, Paragraph, Wrap};
use ratatui::{Frame, Terminal}; use ratatui::{Frame, Terminal};
use upm_core::domain::search::{SearchInstallStatus, SearchResult, SearchResults};
use crate::config::{CliConfig, SearchConfig}; use crate::config::{CliConfig, SearchConfig};

View file

@ -1,15 +1,15 @@
use assert_cmd::Command; use assert_cmd::Command;
use predicates::str::contains; use predicates::str::contains;
use aim_cli::cli::args::Command as AimCommand;
use aim_cli::{Cli, DispatchError};
use aim_core::domain::show::{ShowResultError, SourceSummary};
use aim_core::domain::source::SourceKind;
use clap::Parser; use clap::Parser;
use upm::cli::args::Command as UpmCommand;
use upm::{Cli, DispatchError};
use upm_core::domain::show::{ShowResultError, SourceSummary};
use upm_core::domain::source::SourceKind;
#[test] #[test]
fn help_lists_expected_commands() { fn help_lists_expected_commands() {
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("--help") cmd.arg("--help")
.assert() .assert()
.success() .success()
@ -22,20 +22,20 @@ fn help_lists_expected_commands() {
#[test] #[test]
fn cli_parses_show_subcommand() { fn cli_parses_show_subcommand() {
let cli = Cli::try_parse_from(["upm", "show", "legacy-bat"]).unwrap(); let cli = Cli::try_parse_from(["aim", "show", "legacy-bat"]).unwrap();
match cli.command { match cli.command {
Some(UpmCommand::Show { value }) => assert_eq!(value.as_deref(), Some("legacy-bat")), Some(AimCommand::Show { value }) => assert_eq!(value.as_deref(), Some("legacy-bat")),
other => panic!("expected show command, got {other:?}"), other => panic!("expected show command, got {other:?}"),
} }
} }
#[test] #[test]
fn cli_parses_bare_show_subcommand() { fn cli_parses_bare_show_subcommand() {
let cli = Cli::try_parse_from(["upm", "show"]).unwrap(); let cli = Cli::try_parse_from(["aim", "show"]).unwrap();
match cli.command { match cli.command {
Some(UpmCommand::Show { value }) => assert_eq!(value, None), Some(AimCommand::Show { value }) => assert_eq!(value, None),
other => panic!("expected bare show command, got {other:?}"), other => panic!("expected bare show command, got {other:?}"),
} }
} }

View file

@ -2,6 +2,6 @@ use assert_cmd::Command;
#[test] #[test]
fn cli_shows_help() { fn cli_shows_help() {
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("--help").assert().success(); cmd.arg("--help").assert().success();
} }

View file

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

View file

@ -1,21 +1,21 @@
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use aim_core::registry::model::Registry;
use aim_core::registry::store::RegistryStore;
use assert_cmd::Command; use assert_cmd::Command;
use predicates::prelude::PredicateBooleanExt; use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains; use predicates::str::contains;
use tempfile::tempdir; use tempfile::tempdir;
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use upm_core::registry::model::Registry;
use upm_core::registry::store::RegistryStore;
const FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE"; const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE";
#[test] #[test]
fn list_command_runs_without_registry_entries() { fn list_command_runs_without_registry_entries() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("list") cmd.arg("list")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.assert() .assert()
.success() .success()
.stdout(contains("No installed apps yet")); .stdout(contains("No installed apps yet"));
@ -31,10 +31,10 @@ fn list_command_reads_registered_apps_from_registry_file() {
) )
.unwrap(); .unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("list") cmd.arg("list")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.assert() .assert()
.success() .success()
.stdout(contains("Name")) .stdout(contains("Name"))
@ -54,10 +54,10 @@ fn remove_command_removes_registered_app_from_registry_file() {
) )
.unwrap(); .unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["remove", "bat"]) cmd.args(["remove", "bat"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.assert() .assert()
.success() .success()
.stdout(contains("Removed Bat")) .stdout(contains("Removed Bat"))
@ -73,14 +73,14 @@ fn remove_command_uninstalls_managed_files() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let install_home = dir.path().join("install-home"); let install_home = dir.path().join("install-home");
let payload_path = install_home.join(".local/lib/upm/appimages/sharkdp-bat.AppImage"); let payload_path = install_home.join(".local/lib/aim/appimages/sharkdp-bat.AppImage");
let desktop_path = install_home.join(".local/share/applications/upm-sharkdp-bat.desktop"); let desktop_path = install_home.join(".local/share/applications/aim-sharkdp-bat.desktop");
let icon_path = install_home.join(".local/share/icons/hicolor/256x256/apps/sharkdp-bat.png"); let icon_path = install_home.join(".local/share/icons/hicolor/256x256/apps/sharkdp-bat.png");
let mut add_cmd = Command::cargo_bin("upm").unwrap(); let mut add_cmd = Command::cargo_bin("aim").unwrap();
add_cmd add_cmd
.arg("sharkdp/bat") .arg("sharkdp/bat")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success(); .success();
@ -89,10 +89,10 @@ fn remove_command_uninstalls_managed_files() {
assert!(desktop_path.exists()); assert!(desktop_path.exists());
assert!(icon_path.exists()); assert!(icon_path.exists());
let mut remove_cmd = Command::cargo_bin("upm").unwrap(); let mut remove_cmd = Command::cargo_bin("aim").unwrap();
remove_cmd remove_cmd
.args(["remove", "sharkdp-bat"]) .args(["remove", "sharkdp-bat"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.assert() .assert()
.success() .success()
.stdout(contains("\nRemoved bat")) .stdout(contains("\nRemoved bat"))
@ -101,7 +101,7 @@ fn remove_command_uninstalls_managed_files() {
.stdout(contains("Removed app:").not()) .stdout(contains("Removed app:").not())
.stdout(contains("Removed files")) .stdout(contains("Removed files"))
.stdout(contains("sharkdp-bat.AppImage")) .stdout(contains("sharkdp-bat.AppImage"))
.stdout(contains("upm-sharkdp-bat.desktop")) .stdout(contains("aim-sharkdp-bat.desktop"))
.stdout(contains("sharkdp-bat.png")); .stdout(contains("sharkdp-bat.png"));
assert!(!payload_path.exists()); assert!(!payload_path.exists());
@ -113,10 +113,10 @@ fn remove_command_uninstalls_managed_files() {
fn query_command_registers_unambiguous_app_in_registry_file() { fn query_command_registers_unambiguous_app_in_registry_file() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("sharkdp/bat") cmd.arg("sharkdp/bat")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -140,10 +140,10 @@ fn query_command_registers_unambiguous_app_in_registry_file() {
fn old_release_query_renders_tracking_prompt_without_writing_registry() { fn old_release_query_renders_tracking_prompt_without_writing_registry() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage") cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -158,12 +158,12 @@ fn old_release_query_renders_tracking_prompt_without_writing_registry() {
fn old_release_query_can_track_latest_and_register_app() { fn old_release_query_can_track_latest_and_register_app() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage") cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.env("UPM_TRACKING_PREFERENCE", "latest") .env("AIM_TRACKING_PREFERENCE", "latest")
.assert() .assert()
.success() .success()
.stdout(contains("\nInstalled t3code (user)")) .stdout(contains("\nInstalled t3code (user)"))
@ -182,34 +182,14 @@ fn old_release_query_can_track_latest_and_register_app() {
assert!(contents.contains("locator = \"pingdotgg/t3code\"")); assert!(contents.contains("locator = \"pingdotgg/t3code\""));
} }
#[test]
fn old_release_query_ignores_legacy_tracking_preference_env() {
let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap();
cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage")
.env("UPM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1")
.env("AIM_TRACKING_PREFERENCE", "latest")
.assert()
.success()
.stdout(contains("Choose update tracking"))
.stdout(contains("v0.0.11"))
.stdout(contains("v0.0.12"))
.stdout(contains("Installed t3code").not());
assert!(!registry_path.exists());
}
#[test] #[test]
fn cli_add_installs_and_renders_resolved_mode() { fn cli_add_installs_and_renders_resolved_mode() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("sharkdp/bat") cmd.arg("sharkdp/bat")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -224,10 +204,10 @@ fn cli_add_installs_and_renders_resolved_mode() {
fn positional_query_falls_back_to_search_for_plain_name_queries() { fn positional_query_falls_back_to_search_for_plain_name_queries() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("firefox") cmd.arg("firefox")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -246,10 +226,10 @@ fn positional_query_falls_back_to_search_for_plain_name_queries() {
fn positional_query_falls_back_to_empty_search_when_direct_item_has_no_appimage() { fn positional_query_falls_back_to_empty_search_when_direct_item_has_no_appimage() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("appimagehub/2337998") cmd.arg("appimagehub/2337998")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -266,10 +246,10 @@ fn positional_query_falls_back_to_empty_search_when_direct_item_has_no_appimage(
fn cli_add_installs_appimagehub_source_with_truthful_origin() { fn cli_add_installs_appimagehub_source_with_truthful_origin() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("appimagehub/2338455") cmd.arg("appimagehub/2338455")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -293,10 +273,10 @@ fn cli_add_installs_appimagehub_source_with_truthful_origin() {
fn cli_add_installs_gitlab_source_with_truthful_origin() { fn cli_add_installs_gitlab_source_with_truthful_origin() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("https://gitlab.com/example/team-app") cmd.arg("https://gitlab.com/example/team-app")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -318,10 +298,10 @@ fn cli_add_preserves_direct_url_origin_for_provider_like_downloads() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let query = "https://sourceforge.net/projects/team-app/files/team-app-1.0.0.AppImage/download"; let query = "https://sourceforge.net/projects/team-app/files/team-app-1.0.0.AppImage/download";
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg(query) cmd.arg(query)
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -341,10 +321,10 @@ fn cli_add_installs_sourceforge_latest_download_with_truthful_origin() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let query = "https://sourceforge.net/projects/team-app/files/latest/download"; let query = "https://sourceforge.net/projects/team-app/files/latest/download";
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg(query) cmd.arg(query)
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -363,10 +343,10 @@ fn cli_add_installs_sourceforge_latest_download_with_truthful_origin() {
fn cli_rejects_insecure_http_direct_urls_by_default() { fn cli_rejects_insecure_http_direct_urls_by_default() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("http://example.com/team-app.AppImage") cmd.arg("http://example.com/team-app.AppImage")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.assert() .assert()
.failure() .failure()
.stderr(contains("insecure HTTP sources are disabled")); .stderr(contains("insecure HTTP sources are disabled"));
@ -380,11 +360,11 @@ fn cli_allows_insecure_http_direct_urls_when_config_enables_it() {
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let config_path = dir.path().join("config.toml"); let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "allow_http = true\n").unwrap(); std::fs::write(&config_path, "allow_http = true\n").unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("http://example.com/team-app.AppImage") cmd.arg("http://example.com/team-app.AppImage")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env("UPM_CONFIG_PATH", &config_path) .env("AIM_CONFIG_PATH", &config_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -400,13 +380,13 @@ fn cli_rejects_insecure_appimagehub_download_urls_even_when_http_is_allowed() {
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let config_path = dir.path().join("config.toml"); let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "allow_http = true\n").unwrap(); std::fs::write(&config_path, "allow_http = true\n").unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("appimagehub/2338455") cmd.arg("appimagehub/2338455")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env("UPM_CONFIG_PATH", &config_path) .env("AIM_CONFIG_PATH", &config_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.env("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP", "1") .env("AIM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP", "1")
.assert() .assert()
.failure() .failure()
.stderr(contains("insecure appimagehub download url")); .stderr(contains("insecure appimagehub download url"));
@ -416,12 +396,12 @@ fn cli_rejects_insecure_appimagehub_download_urls_even_when_http_is_allowed() {
fn cli_rejects_appimagehub_install_when_md5_does_not_match() { fn cli_rejects_appimagehub_install_when_md5_does_not_match() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("appimagehub/2338455") cmd.arg("appimagehub/2338455")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.env("UPM_APPIMAGEHUB_FIXTURE_BAD_MD5", "1") .env("AIM_APPIMAGEHUB_FIXTURE_BAD_MD5", "1")
.assert() .assert()
.failure() .failure()
.stderr(contains("weak provider checksum did not match")); .stderr(contains("weak provider checksum did not match"));
@ -432,10 +412,10 @@ fn cli_add_installs_sourceforge_release_folder_with_truthful_origin() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let query = "https://sourceforge.net/projects/team-app/files/releases/beta/download"; let query = "https://sourceforge.net/projects/team-app/files/releases/beta/download";
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg(query) cmd.arg(query)
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -456,10 +436,10 @@ fn cli_add_file_like_sourceforge_release_download_stores_releases_root_and_prese
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let query = let query =
"https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download"; "https://sourceforge.net/projects/team-app/files/releases/team-app-1.0.0.AppImage/download";
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg(query) cmd.arg(query)
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -482,10 +462,10 @@ fn cli_add_file_like_sourceforge_release_download_stores_releases_root_and_prese
fn cli_reports_unsupported_source_queries_distinctly() { fn cli_reports_unsupported_source_queries_distinctly() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("https://gitlab.com/example") cmd.arg("https://gitlab.com/example")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -499,10 +479,10 @@ fn cli_reports_unsupported_source_queries_distinctly() {
fn cli_reports_supported_sources_without_installable_artifacts_distinctly() { fn cli_reports_supported_sources_without_installable_artifacts_distinctly() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("https://sourceforge.net/projects/team-app/") cmd.arg("https://sourceforge.net/projects/team-app/")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -516,10 +496,10 @@ fn cli_reports_supported_sources_without_installable_artifacts_distinctly() {
fn cli_add_emits_live_progress_to_stderr() { fn cli_add_emits_live_progress_to_stderr() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("sharkdp/bat") cmd.arg("sharkdp/bat")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -537,7 +517,7 @@ fn cli_add_emits_live_progress_to_stderr() {
} }
#[test] #[test]
fn bare_upm_review_renders_review_heading() { fn bare_aim_review_renders_review_heading() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let store = RegistryStore::new(registry_path.clone()); let store = RegistryStore::new(registry_path.clone());
@ -562,9 +542,9 @@ fn bare_upm_review_renders_review_heading() {
}) })
.unwrap(); .unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.env("UPM_REGISTRY_PATH", &registry_path) cmd.env("AIM_REGISTRY_PATH", &registry_path)
.assert() .assert()
.success() .success()
.stdout(contains("Update Review")) .stdout(contains("Update Review"))
@ -581,10 +561,10 @@ fn remove_command_emits_live_progress_to_stderr() {
) )
.unwrap(); .unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["remove", "bat"]) cmd.args(["remove", "bat"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.assert() .assert()
.success() .success()
.stderr(contains("Removing bat")) .stderr(contains("Removing bat"))
@ -599,11 +579,11 @@ fn system_request_on_immutable_host_falls_back_to_user_install() {
let os_release_path = dir.path().join("os-release"); let os_release_path = dir.path().join("os-release");
std::fs::write(&os_release_path, "ID=fedora\nVARIANT_ID=silverblue\n").unwrap(); std::fs::write(&os_release_path, "ID=fedora\nVARIANT_ID=silverblue\n").unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["--system", "sharkdp/bat"]) cmd.args(["--system", "sharkdp/bat"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env("UPM_OS_RELEASE_PATH", &os_release_path) .env("AIM_OS_RELEASE_PATH", &os_release_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -617,7 +597,7 @@ fn update_command_applies_updates() {
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let payload_path = dir let payload_path = dir
.path() .path()
.join("install-home/.local/lib/upm/appimages/pingdotgg-t3code.AppImage"); .join("install-home/.local/lib/aim/appimages/pingdotgg-t3code.AppImage");
let store = RegistryStore::new(registry_path.clone()); let store = RegistryStore::new(registry_path.clone());
store store
.save(&Registry { .save(&Registry {
@ -640,10 +620,10 @@ fn update_command_applies_updates() {
}) })
.unwrap(); .unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("update") cmd.arg("update")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -683,10 +663,10 @@ fn update_command_emits_live_progress_to_stderr() {
}) })
.unwrap(); .unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("update") cmd.arg("update")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -702,7 +682,7 @@ fn update_command_reports_when_previous_installation_is_restored() {
let install_home = dir.path().join("install-home"); let install_home = dir.path().join("install-home");
let store = RegistryStore::new(registry_path.clone()); let store = RegistryStore::new(registry_path.clone());
let stable_id = "url-example.com-downloads-team-app.appimage"; let stable_id = "url-example.com-downloads-team-app.appimage";
let payload_path = install_home.join(format!(".local/lib/upm/appimages/{stable_id}.AppImage")); let payload_path = install_home.join(format!(".local/lib/aim/appimages/{stable_id}.AppImage"));
std::fs::create_dir_all(payload_path.parent().unwrap()).unwrap(); std::fs::create_dir_all(payload_path.parent().unwrap()).unwrap();
std::fs::write(&payload_path, b"previous-payload").unwrap(); std::fs::write(&payload_path, b"previous-payload").unwrap();
@ -716,11 +696,11 @@ fn update_command_reports_when_previous_installation_is_restored() {
stable_id: stable_id.to_owned(), stable_id: stable_id.to_owned(),
display_name: "https://example.com/downloads/team-app.AppImage".to_owned(), display_name: "https://example.com/downloads/team-app.AppImage".to_owned(),
source_input: Some("https://example.com/downloads/team-app.AppImage".to_owned()), source_input: Some("https://example.com/downloads/team-app.AppImage".to_owned()),
source: Some(upm_core::domain::source::SourceRef { source: Some(aim_core::domain::source::SourceRef {
kind: upm_core::domain::source::SourceKind::DirectUrl, kind: aim_core::domain::source::SourceKind::DirectUrl,
locator: "https://example.com/downloads/team-app.AppImage".to_owned(), locator: "https://example.com/downloads/team-app.AppImage".to_owned(),
input_kind: upm_core::domain::source::SourceInputKind::DirectUrl, input_kind: aim_core::domain::source::SourceInputKind::DirectUrl,
normalized_kind: upm_core::domain::source::NormalizedSourceKind::DirectUrl, normalized_kind: aim_core::domain::source::NormalizedSourceKind::DirectUrl,
canonical_locator: None, canonical_locator: None,
requested_tag: None, requested_tag: None,
requested_asset_name: None, requested_asset_name: None,
@ -739,10 +719,10 @@ fn update_command_reports_when_previous_installation_is_restored() {
}) })
.unwrap(); .unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.arg("update") cmd.arg("update")
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.env("DISPLAY", ":99") .env("DISPLAY", ":99")
.env("XDG_CURRENT_DESKTOP", "test") .env("XDG_CURRENT_DESKTOP", "test")

View file

@ -1,6 +1,6 @@
use upm::config::SearchConfig; use aim_cli::config::SearchConfig;
use upm::ui::search_browser::{BrowserPhase, SearchBrowserState, SubmitAction}; use aim_cli::ui::search_browser::{BrowserPhase, SearchBrowserState, SubmitAction};
use upm_core::domain::search::{SearchInstallStatus, SearchResult}; use aim_core::domain::search::{SearchInstallStatus, SearchResult};
#[test] #[test]
fn browser_defaults_to_bottom_to_top_ordering() { fn browser_defaults_to_bottom_to_top_ordering() {
@ -81,7 +81,7 @@ fn invalid_numeric_input_keeps_last_good_selection_visible() {
#[test] #[test]
fn highlight_segments_marks_matching_query_fragments() { fn highlight_segments_marks_matching_query_fragments() {
let fragments = upm::ui::search_browser::highlight_segments("pingdotgg/t3code", "dotgg"); let fragments = aim_cli::ui::search_browser::highlight_segments("pingdotgg/t3code", "dotgg");
assert_eq!(fragments.len(), 3); assert_eq!(fragments.len(), 3);
assert_eq!(fragments[1].text, "dotgg"); assert_eq!(fragments[1].text, "dotgg");
@ -123,8 +123,8 @@ fn submit_selection_can_skip_confirmation_from_config() {
assert_eq!( assert_eq!(
action, action,
SubmitAction::Confirmed(upm::ui::search_browser::SearchSelection { SubmitAction::Confirmed(aim_cli::ui::search_browser::SearchSelection {
rows: vec![upm::ui::search_browser::SearchRow { rows: vec![aim_cli::ui::search_browser::SearchRow {
status: SearchInstallStatus::Available, status: SearchInstallStatus::Available,
provider_id: "github".to_owned(), provider_id: "github".to_owned(),
display_name: "charlie/app".to_owned(), display_name: "charlie/app".to_owned(),

View file

@ -3,16 +3,16 @@ use predicates::prelude::PredicateBooleanExt;
use predicates::str::contains; use predicates::str::contains;
use tempfile::tempdir; use tempfile::tempdir;
const FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE"; const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE";
#[test] #[test]
fn search_command_renders_remote_github_results() { fn search_command_renders_remote_github_results() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["search", "bat"]) cmd.args(["search", "bat"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -46,10 +46,10 @@ fn search_command_renders_local_matches_in_deterministic_order() {
) )
.unwrap(); .unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["search", "bat"]) cmd.args(["search", "bat"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -69,10 +69,10 @@ fn search_command_is_read_only_for_registry_contents() {
let original = "version = 1\n[[apps]]\nstable_id = \"bat\"\ndisplay_name = \"Bat\"\n"; let original = "version = 1\n[[apps]]\nstable_id = \"bat\"\ndisplay_name = \"Bat\"\n";
std::fs::write(&registry_path, original).unwrap(); std::fs::write(&registry_path, original).unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["search", "bat"]) cmd.args(["search", "bat"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success(); .success();
@ -88,11 +88,11 @@ fn search_command_fails_fast_on_malformed_config() {
let config_path = dir.path().join("config.toml"); let config_path = dir.path().join("config.toml");
std::fs::write(&config_path, "[search\nskip_confirmation = true\n").unwrap(); std::fs::write(&config_path, "[search\nskip_confirmation = true\n").unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["search", "bat"]) cmd.args(["search", "bat"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env("UPM_CONFIG_PATH", &config_path) .env("AIM_CONFIG_PATH", &config_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.failure() .failure()
@ -110,11 +110,11 @@ fn search_command_uses_plain_text_output_when_not_on_a_tty() {
) )
.unwrap(); .unwrap();
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["search", "bat"]) cmd.args(["search", "bat"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env("UPM_CONFIG_PATH", &config_path) .env("AIM_CONFIG_PATH", &config_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -127,10 +127,10 @@ fn search_command_uses_plain_text_output_when_not_on_a_tty() {
fn search_command_reports_loading_status_to_stderr() { fn search_command_reports_loading_status_to_stderr() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["search", "bat"]) cmd.args(["search", "bat"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -141,10 +141,10 @@ fn search_command_reports_loading_status_to_stderr() {
fn search_command_keeps_empty_results_in_plain_text_mode() { fn search_command_keeps_empty_results_in_plain_text_mode() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["search", "no-such-app-image-query"]) cmd.args(["search", "no-such-app-image-query"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()
@ -156,10 +156,10 @@ fn search_command_keeps_empty_results_in_plain_text_mode() {
fn search_command_renders_appimagehub_results() { fn search_command_renders_appimagehub_results() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let registry_path = dir.path().join("registry.toml"); let registry_path = dir.path().join("registry.toml");
let mut cmd = Command::cargo_bin("upm").unwrap(); let mut cmd = Command::cargo_bin("aim").unwrap();
cmd.args(["search", "firefox"]) cmd.args(["search", "firefox"])
.env("UPM_REGISTRY_PATH", &registry_path) .env("AIM_REGISTRY_PATH", &registry_path)
.env(FIXTURE_MODE_ENV, "1") .env(FIXTURE_MODE_ENV, "1")
.assert() .assert()
.success() .success()

View file

@ -1,28 +1,28 @@
use upm::DispatchResult; use aim_cli::DispatchResult;
use upm::ui::prompt::render_interaction; use aim_cli::ui::prompt::render_interaction;
use upm::ui::render::{render_dispatch_result, render_update_summary}; use aim_cli::ui::render::{render_dispatch_result, render_update_summary};
use upm::ui::search_browser::{SearchRow, format_search_row, render_confirmation_summary}; use aim_cli::ui::search_browser::{SearchRow, format_search_row, render_confirmation_summary};
use upm_core::app::add::InstalledApp; use aim_core::app::add::InstalledApp;
use upm_core::app::interaction::{InteractionKind, InteractionRequest}; use aim_core::app::interaction::{InteractionKind, InteractionRequest};
use upm_core::app::list::ListRow; use aim_core::app::list::ListRow;
use upm_core::app::remove::{RemovalPlan, RemovalResult}; use aim_core::app::remove::{RemovalPlan, RemovalResult};
use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
use upm_core::domain::search::SearchInstallStatus; use aim_core::domain::search::SearchInstallStatus;
use upm_core::domain::show::{ use aim_core::domain::show::{
InstalledShow, MetadataSummary, RemoteArtifactSummary, RemoteShow, ShowResult, SourceSummary, InstalledShow, MetadataSummary, RemoteArtifactSummary, RemoteShow, ShowResult, SourceSummary,
TrackedInstallPaths, UpdateChannelSummary, UpdateStrategySummary, TrackedInstallPaths, UpdateChannelSummary, UpdateStrategySummary,
}; };
use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef};
use upm_core::domain::update::ArtifactCandidate; use aim_core::domain::update::ArtifactCandidate;
use upm_core::domain::update::{ use aim_core::domain::update::{
ChannelPreference, ParsedMetadataKind, PlannedUpdate, UpdateChannelKind, UpdatePlan, ChannelPreference, ParsedMetadataKind, PlannedUpdate, UpdateChannelKind, UpdatePlan,
}; };
use upm_core::integration::install::InstallOutcome; use aim_core::integration::install::InstallOutcome;
fn muted_bold_label(title: &str) -> String { fn muted_bold_label(title: &str) -> String {
let mut style = upm::ui::theme::current_theme().muted; let mut style = aim_cli::ui::theme::current_theme().muted;
style.bold = true; style.bold = true;
upm::ui::theme::apply_style_spec(&format!("{title}:"), &style) aim_cli::ui::theme::apply_style_spec(&format!("{title}:"), &style)
} }
#[test] #[test]
@ -87,13 +87,13 @@ fn removal_summary_lists_removed_files() {
stable_id: "bat".to_owned(), stable_id: "bat".to_owned(),
display_name: "Bat".to_owned(), display_name: "Bat".to_owned(),
artifact_paths: vec![ artifact_paths: vec![
"/tmp/install-home/.local/lib/upm/appimages/bat.AppImage".to_owned(), "/tmp/install-home/.local/lib/aim/appimages/bat.AppImage".to_owned(),
"/tmp/install-home/.local/share/applications/upm-bat.desktop".to_owned(), "/tmp/install-home/.local/share/applications/aim-bat.desktop".to_owned(),
], ],
}, },
removed_paths: vec![ removed_paths: vec![
"/tmp/install-home/.local/lib/upm/appimages/bat.AppImage".to_owned(), "/tmp/install-home/.local/lib/aim/appimages/bat.AppImage".to_owned(),
"/tmp/install-home/.local/share/applications/upm-bat.desktop".to_owned(), "/tmp/install-home/.local/share/applications/aim-bat.desktop".to_owned(),
], ],
remaining_apps: Vec::new(), remaining_apps: Vec::new(),
warnings: Vec::new(), warnings: Vec::new(),
@ -101,7 +101,7 @@ fn removal_summary_lists_removed_files() {
assert!(output.contains("Removed files")); assert!(output.contains("Removed files"));
assert!(output.contains("bat.AppImage")); assert!(output.contains("bat.AppImage"));
assert!(output.contains("upm-bat.desktop")); assert!(output.contains("aim-bat.desktop"));
} }
#[test] #[test]
@ -146,10 +146,10 @@ fn install_summary_omits_completed_steps_recap() {
install: Some(InstallMetadata { install: Some(InstallMetadata {
scope: InstallScope::User, scope: InstallScope::User,
payload_path: Some( payload_path: Some(
"/tmp/install-home/.local/lib/upm/appimages/sharkdp-bat.AppImage".to_owned(), "/tmp/install-home/.local/lib/aim/appimages/sharkdp-bat.AppImage".to_owned(),
), ),
desktop_entry_path: Some( desktop_entry_path: Some(
"/tmp/install-home/.local/share/applications/upm-sharkdp-bat.desktop" "/tmp/install-home/.local/share/applications/aim-sharkdp-bat.desktop"
.to_owned(), .to_owned(),
), ),
icon_path: None, icon_path: None,
@ -176,12 +176,12 @@ fn install_summary_omits_completed_steps_recap() {
tracks_latest: true, tracks_latest: true,
}, },
install_scope: InstallScope::User, install_scope: InstallScope::User,
integration_mode: upm_core::integration::policy::IntegrationMode::Full, integration_mode: aim_core::integration::policy::IntegrationMode::Full,
install_outcome: InstallOutcome { install_outcome: InstallOutcome {
final_payload_path: "/tmp/install-home/.local/lib/upm/appimages/sharkdp-bat.AppImage" final_payload_path: "/tmp/install-home/.local/lib/aim/appimages/sharkdp-bat.AppImage"
.into(), .into(),
desktop_entry_path: Some( desktop_entry_path: Some(
"/tmp/install-home/.local/share/applications/upm-sharkdp-bat.desktop".into(), "/tmp/install-home/.local/share/applications/aim-sharkdp-bat.desktop".into(),
), ),
icon_path: None, icon_path: None,
warnings: Vec::new(), warnings: Vec::new(),
@ -285,8 +285,8 @@ fn installed_show_summary_renders_source_scope_and_paths() {
install_scope: Some(InstallScope::User), install_scope: Some(InstallScope::User),
tracked_paths: TrackedInstallPaths { tracked_paths: TrackedInstallPaths {
payload_path: Some("/tmp/bat.AppImage".to_owned()), payload_path: Some("/tmp/bat.AppImage".to_owned()),
desktop_entry_path: Some("/tmp/upm-bat.desktop".to_owned()), desktop_entry_path: Some("/tmp/aim-bat.desktop".to_owned()),
icon_path: Some("/tmp/upm-bat.png".to_owned()), icon_path: Some("/tmp/aim-bat.png".to_owned()),
}, },
update_strategy: Some(UpdateStrategySummary { update_strategy: Some(UpdateStrategySummary {
preferred: UpdateChannelSummary { preferred: UpdateChannelSummary {
@ -325,30 +325,30 @@ fn installed_show_summary_renders_source_scope_and_paths() {
assert!(output.contains(&format!( assert!(output.contains(&format!(
"{} {}", "{} {}",
muted_bold_label("Source"), muted_bold_label("Source"),
upm::ui::theme::muted("github - sharkdp/bat") aim_cli::ui::theme::muted("github - sharkdp/bat")
))); )));
assert!(output.contains(&format!( assert!(output.contains(&format!(
"{} {}", "{} {}",
muted_bold_label("Update Mechanism"), muted_bold_label("Update Mechanism"),
upm::ui::theme::muted("electron-builder") aim_cli::ui::theme::muted("electron-builder")
))); )));
assert!(output.contains(&format!( assert!(output.contains(&format!(
"{} {}", "{} {}",
muted_bold_label("Architecture"), muted_bold_label("Architecture"),
upm::ui::theme::muted("x86_64") aim_cli::ui::theme::muted("x86_64")
))); )));
assert!(output.contains(&format!( assert!(output.contains(&format!(
"{} {}", "{} {}",
muted_bold_label("Checksum"), muted_bold_label("Checksum"),
upm::ui::theme::muted("sha256:abcdefg...456789") aim_cli::ui::theme::muted("sha256:abcdefg...456789")
))); )));
assert!(output.contains(&muted_bold_label("Installed as User"))); assert!(output.contains(&muted_bold_label("Installed as User")));
assert!(output.contains("/tmp/bat.AppImage")); assert!(output.contains("/tmp/bat.AppImage"));
assert!(output.contains("/tmp/upm-bat.desktop")); assert!(output.contains("/tmp/aim-bat.desktop"));
assert!(!output.contains("[up to date] User")); assert!(!output.contains("[up to date] User"));
assert!(!output.contains("past version")); assert!(!output.contains("past version"));
assert!(!output.contains(&upm::ui::theme::label("Metadata"))); assert!(!output.contains(&aim_cli::ui::theme::label("Metadata")));
assert!(!output.contains(&upm::ui::theme::label("Files"))); assert!(!output.contains(&aim_cli::ui::theme::label("Files")));
assert!(!output.contains("abcdefghijklmnopqrstuvwxyz0123456789")); assert!(!output.contains("abcdefghijklmnopqrstuvwxyz0123456789"));
} }
@ -427,24 +427,24 @@ fn installed_show_summary_reports_when_newer_versions_are_available() {
assert!(output.contains(&format!( assert!(output.contains(&format!(
"{} {}", "{} {}",
muted_bold_label("Source"), muted_bold_label("Source"),
upm::ui::theme::muted("github - pingdotgg/t3code") aim_cli::ui::theme::muted("github - pingdotgg/t3code")
))); )));
assert!(output.contains(&format!( assert!(output.contains(&format!(
"{} {}", "{} {}",
muted_bold_label("Update Mechanism"), muted_bold_label("Update Mechanism"),
upm::ui::theme::muted("electron-builder") aim_cli::ui::theme::muted("electron-builder")
))); )));
assert!(output.contains(&format!( assert!(output.contains(&format!(
"{} {}", "{} {}",
muted_bold_label("Architecture"), muted_bold_label("Architecture"),
upm::ui::theme::muted("x86_64") aim_cli::ui::theme::muted("x86_64")
))); )));
assert!(output.contains(&muted_bold_label("Installed as User"))); assert!(output.contains(&muted_bold_label("Installed as User")));
assert!(!output.contains("[update available] User")); assert!(!output.contains("[update available] User"));
assert!(!output.contains("past versions")); assert!(!output.contains("past versions"));
assert!(!output.contains("latest v0.0.16")); assert!(!output.contains("latest v0.0.16"));
assert!(!output.contains(&upm::ui::theme::label("Metadata"))); assert!(!output.contains(&aim_cli::ui::theme::label("Metadata")));
assert!(!output.contains(&upm::ui::theme::label("Files"))); assert!(!output.contains(&aim_cli::ui::theme::label("Files")));
} }
#[test] #[test]
@ -463,7 +463,7 @@ fn installed_show_list_renders_each_app_using_singular_show_format() {
install_scope: Some(InstallScope::User), install_scope: Some(InstallScope::User),
tracked_paths: TrackedInstallPaths { tracked_paths: TrackedInstallPaths {
payload_path: Some("/tmp/bat.AppImage".to_owned()), payload_path: Some("/tmp/bat.AppImage".to_owned()),
desktop_entry_path: Some("/tmp/upm-bat.desktop".to_owned()), desktop_entry_path: Some("/tmp/aim-bat.desktop".to_owned()),
icon_path: None, icon_path: None,
}, },
update_strategy: None, update_strategy: None,
@ -512,12 +512,12 @@ fn installed_show_list_renders_each_app_using_singular_show_format() {
assert!(output.contains(&format!( assert!(output.contains(&format!(
"{} {}", "{} {}",
muted_bold_label("Source"), muted_bold_label("Source"),
upm::ui::theme::muted("github - sharkdp/bat") aim_cli::ui::theme::muted("github - sharkdp/bat")
))); )));
assert!(output.contains(&format!( assert!(output.contains(&format!(
"{} {}", "{} {}",
muted_bold_label("Source"), muted_bold_label("Source"),
upm::ui::theme::muted("github - pingdotgg/t3code") aim_cli::ui::theme::muted("github - pingdotgg/t3code")
))); )));
} }

View file

@ -1,5 +1,5 @@
[package] [package]
name = "upm-core" name = "aim-core"
version.workspace = true version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
@ -17,8 +17,6 @@ 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

View file

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

View file

@ -59,8 +59,7 @@ pub trait SourceAdapter {
} }
fn supports_source(&self, source: &SourceRef) -> bool { fn supports_source(&self, source: &SourceRef) -> bool {
self.repository_source_kind() == Some(source.kind) crate::adapters::supports_source(self, source)
|| self.exact_source_kind() == Some(source.kind)
} }
fn resolve_source(&self, source: &SourceRef) -> Result<AdapterResolveOutcome, AdapterError> { fn resolve_source(&self, source: &SourceRef) -> Result<AdapterResolveOutcome, AdapterError> {

View file

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

View file

@ -1,15 +1,11 @@
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;
pub mod progress; pub mod progress;
pub mod providers;
pub mod query; pub mod query;
pub mod remove; pub mod remove;
pub mod scope; 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};

View file

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

View file

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

View file

@ -1,7 +1,4 @@
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 {
@ -34,6 +31,25 @@ 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,
@ -50,6 +66,30 @@ 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>,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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