diff --git a/.architecture/overview.md b/.architecture/overview.md index bd0a091..2e648d9 100644 --- a/.architecture/overview.md +++ b/.architecture/overview.md @@ -2,92 +2,25 @@ ## 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/upm`: the CLI frontend over `upm-core`. It handles argument parsing, config loading, terminal UX, prompting, progress reporting, summary rendering, and config-driven module presentation. -- `crates/upm-appimage`: 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. +- `crates/aim-core`: source normalization, provider adapters, install/update planning, payload installation, registry persistence, and desktop integration. +- `crates/aim-cli`: argument parsing, config loading, terminal UX, prompting, progress reporting, and summary rendering. -The intended split is strict: - -- `upm-core` is effectively the application -- `upm` is one frontend over that application -- `upm-ui` will be another frontend over that application -- package-manager modules own their own implementation detail and speak to `upm-core` through normalized traits - -That keeps frontend-agnostic logic in `upm-core`, makes a future GUI a first-class consumer instead of a later retrofit, and prevents frontend layers from accumulating package-manager-specific behavior. - -## Application Boundary - -The architectural boundary is: - -- `upm` may know which modules exist for configuration, enablement, disablement, priority, and display -- `upm-ui` should operate under the same rule as the CLI: it talks to `upm-core`, not directly to modules -- `upm` must not talk directly to a package-manager module or implement module-specific logic -- `upm-core` owns the unified application interface used by the CLI now and a GUI later -- `upm-core` owns module registration, composition, enablement checks, and request fan-out -- `upm-core` fans requests out to enabled modules and aggregates normalized results -- each module owns its own internal backends, source quirks, artifact selection, and provider-specific rules - -In practical terms, `upm-core` is where the product behavior lives. The CLI should remain replaceable. - -## Public API Shape - -`upm-core` should expose one high-level application facade to frontend crates. - -- the public boundary should be an application-facing type such as `UpmApp` -- the facade should present operations like search, add, show, update, remove, and config management in product terms -- frontends should not compose lower-level orchestration services themselves - -That public facade should stay thin. The internal implementation in `upm-core` can and should be split into smaller services such as: - -- module registry and module loading -- search orchestration -- add planning and execution -- show resolution -- update planning and execution -- configuration and state services - -This gives both frontends one stable application boundary without turning the facade into a god object. The orchestration depth stays inside `upm-core`, where it belongs. - -## Module Tree - -The intended tree is: - -- `upm-core` - - public application facade - - internal orchestration services - - module registry and composition - - normalized contracts for package-manager modules -- frontend crates - - `upm` for CLI concerns only - - `upm-ui` for GUI concerns only -- module crates - - `upm-appimage` - - AppImageHub backend - - GitHub-backed AppImage acquisition - - GitLab-backed AppImage acquisition - - SourceForge-backed AppImage acquisition - - direct AppImage URL handling - - AppImage-specific artifact and metadata rules - -The important constraint is that the top layer understands package-manager modules, not the inner mechanics of how each module finds or resolves artifacts. +The split keeps product logic in `aim-core` so additional frontends can reuse the same install and update pipeline. ## Core Flow The main execution path is: -1. Parse CLI input and load runtime config in `upm`. -2. Call the unified application facade in `upm-core`. -3. Let `upm-core` route the request into internal orchestration services. -4. Let those services select enabled modules and fan the request out through normalized module traits. -5. Aggregate normalized results into an add, show, update, search, or remove flow. -6. Download the selected AppImage into a staged path when the chosen module requires it. -7. Verify integrity metadata when available. -8. Commit the payload into the managed install location. -9. Write desktop integration artifacts and refresh helper caches. -10. Persist registry state atomically. +1. Parse CLI input and load runtime config in `aim-cli`. +2. Resolve the query into a normalized source in `aim-core`. +3. Build an add or update plan through provider adapters and artifact selection. +4. Download the selected AppImage into a staged path. +5. Verify integrity metadata when available. +6. Commit the payload into the managed install location. +7. Write desktop integration artifacts and refresh helper caches. +8. Persist registry state atomically. ## Source And Provider Model @@ -100,18 +33,7 @@ Supported source classes currently include: - direct URLs - local file imports -Core orchestration and normalized module contracts live in `crates/upm-core`. Package-manager-specific behavior belongs in module crates. - -For the AppImage module, that means `crates/upm-appimage` is the package-manager boundary and should grow to own AppImage-specific backing sources internally. `upm-core` should coordinate the module through normalized traits, not absorb AppImageHub, GitHub-backed AppImage discovery, GitLab-backed AppImage discovery, SourceForge-backed AppImage discovery, or direct AppImage URL handling as first-class application concepts. - -## Runtime Interface - -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` +Provider-specific resolution lives in `crates/aim-core/src/adapters` and `crates/aim-core/src/source`. ## Security Hardening State @@ -129,9 +51,9 @@ The remaining deferred AppImageHub host-trust concern is tracked in `security-is ## Persistence And Integration -- Registry writes are atomic and live under the registry store implementation in `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. -- 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 diff --git a/.architecture/roadmap.md b/.architecture/roadmap.md deleted file mode 100644 index a4cb431..0000000 --- a/.architecture/roadmap.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/.plans/013-upm-rename-and-core-extraction/2026-03-21-upm-rename-and-core-extraction-implementation-plan.md b/.plans/013-upm-rename-and-core-extraction/2026-03-21-upm-rename-and-core-extraction-implementation-plan.md deleted file mode 100644 index acca473..0000000 --- a/.plans/013-upm-rename-and-core-extraction/2026-03-21-upm-rename-and-core-extraction-implementation-plan.md +++ /dev/null @@ -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-.desktop` - -**Step 2: Run the focused tests to verify failure** - -Run: - -```bash -cargo test --package aim-cli --test cli_smoke -cargo test --package aim-cli --test config_loading -cargo test --package aim-core --test install_paths -cargo test --package aim-core --test install_policy -``` - -Expected: FAIL because the workspace still exposes `aim`, `aim-cli`, `aim-core`, and `aim` default paths. - -**Step 3: Perform the crate and manifest rename** - -Run: - -```bash -git mv crates/aim-cli crates/upm -git mv crates/aim-core crates/upm-core -``` - -Then update: - -- workspace members and default members in `Cargo.toml` -- package names to `upm` and `upm-core` -- binary name to `upm` -- crate imports from `aim_core` to `upm_core` -- crate imports from `aim_cli` to `upm` -- clap command name from `aim` to `upm` -- default config, registry, payload-root, and desktop-entry paths to `upm` - -**Step 4: Run the focused tests to verify pass** - -Run: - -```bash -cargo test --package upm --test cli_smoke -cargo test --package upm --test cli_commands -cargo test --package upm --test config_loading -cargo test --package upm-core --test install_paths -cargo test --package upm-core --test install_policy -``` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add Cargo.toml crates/upm crates/upm-core -git commit -m "refactor: rename workspace to upm" -``` - -### Task 2: Remove remaining `aim`-named runtime interfaces - -**Files:** -- Modify: `crates/upm/src/config.rs` -- Modify: `crates/upm/src/cli/config.rs` -- Modify: `crates/upm/src/lib.rs` -- Modify: `crates/upm/src/ui/prompt.rs` -- Modify: `crates/upm-core/src/platform/mod.rs` -- Modify: `crates/upm-core/src/source/github.rs` -- Modify: `crates/upm-core/src/source/appimagehub.rs` -- Modify: `crates/upm-core/src/integration/refresh.rs` -- Test: `crates/upm/tests/config_loading.rs` - -**Step 1: Write the failing strict-rename expectations** - -Update representative tests to cover: - -- config lookup uses `UPM_CONFIG_PATH` -- registry lookup uses `UPM_REGISTRY_PATH` -- old `AIM_*` config and registry overrides are ignored -- tracking preference uses `UPM_TRACKING_PREFERENCE` -- old `AIM_TRACKING_PREFERENCE` is ignored -- provider fixture execution uses the renamed `UPM_*` interfaces through CLI-facing tests -- managed install and summary output use `upm` paths and desktop prefixes - -**Step 2: Run the focused tests to verify failure** - -Run: - -```bash -cargo test --package upm --test config_loading -cargo test --package upm --test search_cli -cargo test --package upm --test end_to_end_cli -cargo test --package upm --test ui_summary -``` - -Expected: FAIL because representative CLI and config flows still depend on old `aim` names. - -**Step 3: Remove the remaining `aim` interfaces** - -Update the codebase so renamed runtime interfaces are consistently `upm`: - -- environment variable names use `UPM_*` -- helper/debug prefixes print `[upm]` -- GitHub user agent identifies as `upm/0.1` -- old `aim` compatibility reads are removed instead of preserved - -**Step 4: Run the focused tests to verify pass** - -Run: - -```bash -cargo test --package upm --test config_loading -cargo test --package upm --test search_cli -cargo test --package upm --test end_to_end_cli -cargo test --package upm --test ui_summary -``` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add crates/upm/src/config.rs crates/upm/src/cli/config.rs crates/upm/src/lib.rs crates/upm/src/ui/prompt.rs crates/upm-core/src/platform/mod.rs crates/upm-core/src/source/github.rs crates/upm-core/src/source/appimagehub.rs crates/upm-core/src/integration/refresh.rs crates/upm/tests/config_loading.rs -git commit -m "refactor: remove remaining aim runtime interfaces" -``` - -### Task 3: Add a provider-composition seam in `upm-core` - -**Files:** -- Create: `crates/upm-core/src/app/providers.rs` -- Modify: `crates/upm-core/src/app/mod.rs` -- Modify: `crates/upm-core/src/app/add.rs` -- Modify: `crates/upm-core/src/app/search.rs` -- Modify: `crates/upm-core/src/lib.rs` -- Create: `crates/upm-core/tests/provider_registry.rs` - -**Step 1: Write the failing provider-composition tests** - -Create `crates/upm-core/tests/provider_registry.rs` with two focused tests: - -- `build_search_results_with_registered_providers_uses_external_hits` using a stub external search provider -- `build_add_plan_with_registered_providers_delegates_appimagehub_like_sources` using a stub external add provider that returns a fixed artifact and release - -The tests should prove that `upm-core` orchestration can consume provider-supplied search and add behavior without hardcoding AppImage-specific modules. - -**Step 2: Run the focused tests to verify failure** - -Run: - -```bash -cargo test --package upm-core --test provider_registry -``` - -Expected: FAIL because the orchestration layer still hardcodes AppImageHub in `app/add.rs` and `app/search.rs`. - -**Step 3: Introduce the narrow composition API** - -Create `crates/upm-core/src/app/providers.rs` with minimal types: - -- `pub trait ExternalAddProvider` -- `pub struct ExternalAddResolution` -- `pub struct ProviderRegistry<'a>` - -Requirements: - -- `ProviderRegistry` carries `search_providers: Vec<&'a dyn SearchProvider>` and `external_add_providers: Vec<&'a dyn ExternalAddProvider>` -- `build_search_results` can delegate to `build_search_results_with` using providers supplied by the caller -- `build_add_plan_with_reporter_and_policy` gets a sibling entrypoint that accepts a `ProviderRegistry` -- core built-ins remain in `upm-core`; only AppImage-specific exact-resolution and search logic should move behind the new registry seam - -Keep the interface intentionally small. Do not attempt plugin loading or dynamic discovery yet. - -**Step 4: Run the focused tests to verify pass** - -Run: - -```bash -cargo test --package upm-core --test provider_registry -``` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add crates/upm-core/src/app/providers.rs crates/upm-core/src/app/mod.rs crates/upm-core/src/app/add.rs crates/upm-core/src/app/search.rs crates/upm-core/src/lib.rs crates/upm-core/tests/provider_registry.rs -git commit -m "refactor: add provider composition seam to upm-core" -``` - -### Task 4: Extract AppImage-specific logic into `upm-appimage` - -**Files:** -- Modify: `Cargo.toml` -- Create: `crates/upm-appimage/Cargo.toml` -- Create: `crates/upm-appimage/src/lib.rs` -- Create: `crates/upm-appimage/src/add.rs` -- Create: `crates/upm-appimage/src/search.rs` -- Create: `crates/upm-appimage/src/source/mod.rs` -- Create: `crates/upm-appimage/src/source/appimagehub.rs` -- Modify: `crates/upm-core/src/adapters/mod.rs` -- Modify: `crates/upm-core/src/source/mod.rs` -- Modify: `crates/upm-core/src/app/add.rs` -- Modify: `crates/upm-core/src/app/search.rs` -- Create: `crates/upm-appimage/tests/appimagehub_search.rs` -- Modify: `crates/upm-core/tests/adapter_contract.rs` -- Modify: `crates/upm-core/tests/adapter_smoke.rs` - -**Step 1: Write the failing extracted-module test** - -Create `crates/upm-appimage/tests/appimagehub_search.rs` by moving the current AppImageHub search expectations out of `upm-core` and updating imports to target the new crate. - -Also update the affected `upm-core` tests so they no longer import `AppImageHubAdapter` from `upm-core` directly. - -**Step 2: Run the focused tests to verify failure** - -Run: - -```bash -cargo test --package upm-appimage --test appimagehub_search -``` - -Expected: FAIL because the new crate does not exist yet. - -**Step 3: Create the new crate and move AppImageHub implementation into it** - -Move the AppImageHub-specific code into the new crate: - -- transport and fixture logic from `upm-core/src/source/appimagehub.rs` -- AppImage-backed exact-resolution logic into `crates/upm-appimage/src/add.rs` implementing `ExternalAddProvider` -- AppImageHub search provider logic out of `upm-core/src/app/search.rs` into `crates/upm-appimage/src/search.rs` - -`upm-appimage` should depend on `upm-core`, not the other way around. - -Leave `SourceKind::AppImageHub`, `SourceInputKind::AppImageHub*`, and `NormalizedSourceKind::AppImageHub` in `upm-core` for this milestone. The deeper provider/domain generalization belongs to the next milestone. - -**Step 4: Remove direct AppImageHub wiring from `upm-core`** - -Update `upm-core` so it no longer declares: - -- `pub mod appimagehub;` in `src/adapters/mod.rs` -- `pub mod appimagehub;` in `src/source/mod.rs` -- built-in AppImageHub search-provider construction in `src/app/search.rs` -- direct `AppImageHubAdapter` imports in `src/app/add.rs` - -After this step, AppImage behavior should exist only through the provider registry seam from Task 3. - -**Step 5: Run the focused tests to verify pass** - -Run: - -```bash -cargo test --package upm-appimage --test appimagehub_search -cargo test --package upm-core --test adapter_contract -cargo test --package upm-core --test adapter_smoke -``` - -Expected: PASS. - -**Step 6: Commit** - -```bash -git add Cargo.toml crates/upm-appimage crates/upm-core/src/adapters/mod.rs crates/upm-core/src/source/mod.rs crates/upm-core/src/app/add.rs crates/upm-core/src/app/search.rs crates/upm-core/tests/adapter_contract.rs crates/upm-core/tests/adapter_smoke.rs -git commit -m "refactor: extract appimage support into upm-appimage" -``` - -### Task 5: Rewire the `upm` CLI to assemble built-in providers from modules - -**Files:** -- Modify: `crates/upm/Cargo.toml` -- Create: `crates/upm/src/providers.rs` -- Modify: `crates/upm/src/lib.rs` -- Test: `crates/upm/tests/search_cli.rs` -- Test: `crates/upm/tests/end_to_end_cli.rs` -- Test: `crates/upm/tests/ui_summary.rs` - -**Step 1: Write the failing CLI integration expectations** - -Update CLI integration tests to prove that: - -- `upm search firefox` still includes AppImageHub results -- direct `upm appimagehub/2338455` install flow still succeeds through the CLI -- the final summary output still renders the new `upm`-prefixed paths and desktop-entry names - -**Step 2: Run the focused tests to verify failure** - -Run: - -```bash -cargo test --package upm --test search_cli -cargo test --package upm --test end_to_end_cli -cargo test --package upm --test ui_summary -``` - -Expected: FAIL because `upm` does not yet assemble AppImage providers through the extracted module. - -**Step 3: Add CLI-side provider assembly** - -Create `crates/upm/src/providers.rs` that: - -- builds the `ProviderRegistry` for `upm-core` -- registers the `upm-appimage` search provider -- registers the `upm-appimage` external add provider - -Update `crates/upm/src/lib.rs` so dispatch paths call the provider-aware core entrypoints instead of hardcoded core defaults. - -Do not move progress rendering or config loading into `upm-core`; the CLI remains the presentation layer. - -**Step 4: Run the focused tests to verify pass** - -Run: - -```bash -cargo test --package upm --test search_cli -cargo test --package upm --test end_to_end_cli -cargo test --package upm --test ui_summary -``` - -Expected: PASS. - -**Step 5: Commit** - -```bash -git add crates/upm/Cargo.toml crates/upm/src/providers.rs crates/upm/src/lib.rs crates/upm/tests/search_cli.rs crates/upm/tests/end_to_end_cli.rs crates/upm/tests/ui_summary.rs -git commit -m "refactor: compose providers from upm modules" -``` - -### Task 6: Update docs and run full workspace verification - -**Files:** -- Modify: `README.md` -- Modify: `.architecture/overview.md` -- Modify: `.architecture/roadmap.md` - -**Step 1: Update product and architecture docs** - -Document: - -- the workspace rename to `upm` -- `upm-core` as the headless application layer -- `upm-appimage` as the first installable provider module -- compatibility behavior for existing `aim` config and registry locations -- the fact that provider composition now happens in the CLI rather than through hardcoded AppImage paths in `upm-core` - -**Step 2: Verify the docs mention the new structure** - -Run: - -```bash -rg -n "upm-core|upm-appimage|legacy aim|ProviderRegistry|upm" README.md .architecture/overview.md .architecture/roadmap.md -``` - -Expected: matches showing the renamed crates, provider split, and compatibility note. - -**Step 3: Run the full verification suite** - -Run: - -```bash -cargo fmt --all -cargo test --workspace -cargo clippy --workspace --all-targets --all-features -- -D warnings -``` - -Expected: PASS. - -**Step 4: Commit** - -```bash -git add README.md .architecture/overview.md .architecture/roadmap.md -git commit -m "docs: describe upm core and module split" -``` \ No newline at end of file diff --git a/.plans/014-appimage-on-new-core/2026-03-21-appimage-on-new-core-implementation-plan.md b/.plans/014-appimage-on-new-core/2026-03-21-appimage-on-new-core-implementation-plan.md deleted file mode 100644 index 5f5b02d..0000000 --- a/.plans/014-appimage-on-new-core/2026-03-21-appimage-on-new-core-implementation-plan.md +++ /dev/null @@ -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" -``` \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index c8956f2..f7cdfcd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,8 +10,4 @@ Audits are to live in `.audits` with a good name slug plus time & date. IMPORTANT TO CHECK BEFORE ANY COMMIT!! Architecture under `.architecture` must be maintained with each change. - Security issues stumbled upon or noticed during execution **already in code** must live in `security-issues.md`. Newly added issues during execution or planning should be raised to the user and/or dealt with, instead of growing the list. - - An overview of the workspace, should live in `overview.md`. - - A roadmap of the planned direction & features in `roadmap.md`. - - ## UPM work - We are currently using the `upm` branch as our "main" for all `upm` work, meaning feature branches/worktrees will merge back into here. \ No newline at end of file + - An overview of the workspace, should live in `overview.md`. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d589261..d60c173 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,41 @@ dependencies = [ "memchr", ] +[[package]] +name = "aim-cli" +version = "0.1.0" +dependencies = [ + "aim-core", + "assert_cmd", + "clap", + "console 0.16.3", + "crossterm", + "dialoguer", + "indicatif", + "libc", + "predicates", + "ratatui", + "serde", + "tempfile", + "toml", +] + +[[package]] +name = "aim-core" +version = "0.1.0" +dependencies = [ + "base64", + "fs2", + "md5", + "quick-xml", + "reqwest", + "serde", + "serde_yaml", + "sha2", + "tempfile", + "toml", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -1931,62 +1966,6 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" -[[package]] -name = "upm" -version = "0.1.0" -dependencies = [ - "assert_cmd", - "clap", - "console 0.16.3", - "crossterm", - "dialoguer", - "indicatif", - "libc", - "predicates", - "ratatui", - "serde", - "tempfile", - "toml", - "upm-appimage", - "upm-core", -] - -[[package]] -name = "upm-appimage" -version = "0.1.0" -dependencies = [ - "quick-xml", - "reqwest", - "serde", - "upm-core", - "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]] name = "url" version = "2.5.8" diff --git a/Cargo.toml b/Cargo.toml index 371e126..a1dc9cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,10 @@ [workspace] members = [ - "crates/upm-module-api", - "crates/upm-core", - "crates/upm-appimage", - "crates/upm", + "crates/aim-core", + "crates/aim-cli", ] default-members = [ - "crates/upm", + "crates/aim-cli", ] resolver = "2" diff --git a/README.md b/README.md index 913ac43..9a056ac 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,24 @@ -# upm -Universal Package Manager +# aim +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 -- `crates/upm-core`: headless application layer for query normalization, orchestration, module registration and composition, registry persistence, install/update planning, and the unified frontend-facing API -- `crates/upm`: thin terminal frontend for argument parsing, config loading, prompting, progress reporting, and summary rendering -- `crates/upm-appimage`: AppImage package-manager module responsible for AppImage-specific acquisition and resolution behavior -- `crates/upm-ui` (planned): GUI frontend over `upm-core` +- `crates/aim-core`: business logic, source adapters, registry, install/update planning +- `crates/aim-cli`: thin terminal frontend for parsing, prompting, and rendering -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 ```text -upm -upm -upm update -upm list -upm search -upm remove +aim +aim +aim update +aim list +aim search +aim remove ``` ## Query Forms @@ -38,22 +36,22 @@ upm remove ## Search -`upm search ` is part of the initial modular module surface. +`aim search ` 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/` -- module composition belongs in `upm-core`, not in the CLI frontend +- the search model is provider-extensible for future phases ## 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` - `--system` ## 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: @@ -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 - 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 -- `upm ` installs direct provider matches when available, otherwise falls back to search results, shows live progress on stderr, prints an `Installation Summary` on stdout for installs, and renders an `Installation Review` when tracking needs confirmation -- bare `upm` prints an `Update Review` without mutating the registry -- `upm update` executes the pending updates, streams live status on stderr, then prints an `Update Summary` -- `upm list` renders either `Installed Apps` or `No installed apps yet` -- `upm remove ` resolves a registered application name, streams removal progress on stderr, then prints a `Removal Summary` +- `aim ` installs direct provider matches when available, otherwise falls back to search results, shows live progress on stderr, prints an `Installation Summary` on stdout for installs, and renders an `Installation Review` when tracking needs confirmation +- bare `aim` prints an `Update Review` without mutating the registry +- `aim update` executes the pending updates, streams live status on stderr, then prints an `Update Summary` +- `aim list` renders either `Installed Apps` or `No installed apps yet` +- `aim remove ` resolves a registered application name, streams removal progress on stderr, then prints a `Removal Summary` ## Terminal UX diff --git a/crates/upm/Cargo.toml b/crates/aim-cli/Cargo.toml similarity index 81% rename from crates/upm/Cargo.toml rename to crates/aim-cli/Cargo.toml index f1e495c..cfc65ec 100644 --- a/crates/upm/Cargo.toml +++ b/crates/aim-cli/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "upm" +name = "aim-cli" version.workspace = true edition.workspace = true license.workspace = true @@ -8,7 +8,7 @@ license.workspace = true path = "src/lib.rs" [[bin]] -name = "upm" +name = "aim" path = "src/main.rs" [dependencies] @@ -21,8 +21,7 @@ libc.workspace = true ratatui.workspace = true serde.workspace = true toml.workspace = true -upm-appimage = { path = "../upm-appimage" } -upm-core = { path = "../upm-core" } +aim-core = { path = "../aim-core" } [dev-dependencies] assert_cmd.workspace = true diff --git a/crates/upm/src/cli/args.rs b/crates/aim-cli/src/cli/args.rs similarity index 89% rename from crates/upm/src/cli/args.rs rename to crates/aim-cli/src/cli/args.rs index 4a8a0a8..1bdc5a3 100644 --- a/crates/upm/src/cli/args.rs +++ b/crates/aim-cli/src/cli/args.rs @@ -1,8 +1,8 @@ use clap::Parser; #[derive(Debug, Parser)] -#[command(name = "upm")] -#[command(about = "Universal Package Manager")] +#[command(name = "aim")] +#[command(about = "AppImage Manager")] pub struct Cli { #[arg(global = true, long = "system", conflicts_with = "user")] pub system: bool, diff --git a/crates/upm/src/cli/config.rs b/crates/aim-cli/src/cli/config.rs similarity index 97% rename from crates/upm/src/cli/config.rs rename to crates/aim-cli/src/cli/config.rs index 6349c77..73a40bd 100644 --- a/crates/upm/src/cli/config.rs +++ b/crates/aim-cli/src/cli/config.rs @@ -52,10 +52,10 @@ struct FileThemeConfig { impl AppConfig { 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") .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()) } diff --git a/crates/upm/src/cli/mod.rs b/crates/aim-cli/src/cli/mod.rs similarity index 100% rename from crates/upm/src/cli/mod.rs rename to crates/aim-cli/src/cli/mod.rs diff --git a/crates/upm/src/config.rs b/crates/aim-cli/src/config.rs similarity index 94% rename from crates/upm/src/config.rs rename to crates/aim-cli/src/config.rs index fcff83a..3397216 100644 --- a/crates/upm/src/config.rs +++ b/crates/aim-cli/src/config.rs @@ -68,16 +68,16 @@ pub fn load_from_path(path: &Path) -> Result { } pub fn default_path() -> PathBuf { - if let Some(path) = env::var_os("UPM_CONFIG_PATH") { + if let Some(path) = env::var_os("AIM_CONFIG_PATH") { return PathBuf::from(path); } 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()); - PathBuf::from(home).join(".config/upm/config.toml") + PathBuf::from(home).join(".config/aim/config.toml") } #[derive(Debug)] diff --git a/crates/upm/src/lib.rs b/crates/aim-cli/src/lib.rs similarity index 70% rename from crates/upm/src/lib.rs rename to crates/aim-cli/src/lib.rs index e6a9cc1..a9e5e6a 100644 --- a/crates/upm/src/lib.rs +++ b/crates/aim-cli/src/lib.rs @@ -1,23 +1,28 @@ pub mod cli; pub mod config; -pub mod providers; pub mod ui; use std::collections::{HashMap, HashSet}; use std::env; use std::path::{Path, PathBuf}; -use upm_core::app::add::{AddPlan, AddSecurityPolicy, InstalledApp, resolve_requested_scope}; -use upm_core::app::list::ListRow; -use upm_core::app::progress::{ +use aim_core::app::add::{ + AddPlan, AddSecurityPolicy, InstalledApp, build_add_plan_with_reporter_and_policy, + 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, }; -use upm_core::app::remove::{RemovalResult, remove_registered_app_with_reporter}; -use upm_core::domain::app::AppRecord; -use upm_core::domain::search::{SearchQuery, SearchResults}; -use upm_core::domain::show::{InstalledShow, ShowResult}; -use upm_core::domain::update::{UpdateExecutionResult, UpdatePlan}; -use upm_core::registry::store::RegistryStore; +use aim_core::app::remove::{RemovalResult, remove_registered_app_with_reporter}; +use aim_core::app::search::build_search_results; +use aim_core::app::show::{build_installed_show_results, build_show_result}; +use aim_core::app::update::{build_update_plan, execute_updates_with_reporter_and_policy}; +use aim_core::domain::app::AppRecord; +use aim_core::domain::search::{SearchQuery, SearchResults}; +use aim_core::domain::show::{InstalledShow, ShowResult}; +use aim_core::domain::update::{UpdateExecutionResult, UpdatePlan}; +use aim_core::registry::store::RegistryStore; pub use cli::args::Cli; @@ -47,15 +52,14 @@ pub fn dispatch_with_reporter_and_config( let store = RegistryStore::new(registry_path); let registry = store.load()?; let apps = registry.apps.clone(); - let app = providers::application(); 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 { 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 } => { let removal = remove_registered_app_with_reporter(&query, &apps, &install_home, reporter)?; @@ -76,7 +80,7 @@ pub fn dispatch_with_reporter_and_config( kind: OperationKind::Search, label: query.clone(), }); - let results = app.search(&SearchQuery::new(&query), &apps)?; + let results = build_search_results(&SearchQuery::new(&query), &apps)?; reporter.report(&OperationEvent::Finished { 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 { Some(value) => { - let result = app.show(&value, &apps)?; + let result = build_show_result(&value, &apps)?; 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 => { - let updates = app.execute_updates( + let updates = execute_updates_with_reporter_and_policy( &apps, &install_home, reporter, @@ -119,8 +123,10 @@ pub fn dispatch_with_reporter_and_config( if let Some(query) = cli.query { let requested_scope = resolve_requested_scope(cli.system, cli.user, is_effective_root()); - let 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, + transport.as_ref(), reporter, AddSecurityPolicy { allow_http_user_sources: config.allow_http, @@ -129,16 +135,16 @@ pub fn dispatch_with_reporter_and_config( let mut plan = match plan_result { Ok(plan) => plan, Err( - upm_core::app::add::BuildAddPlanError::Query( - upm_core::app::query::ResolveQueryError::Unsupported, + aim_core::app::add::BuildAddPlanError::Query( + aim_core::app::query::ResolveQueryError::Unsupported, ) - | upm_core::app::add::BuildAddPlanError::NoInstallableArtifact { .. }, + | aim_core::app::add::BuildAddPlanError::NoInstallableArtifact { .. }, ) => { reporter.report(&OperationEvent::Started { kind: OperationKind::Search, label: query.clone(), }); - let results = app.search(&SearchQuery::new(&query), &apps)?; + let results = build_search_results(&SearchQuery::new(&query), &apps)?; reporter.report(&OperationEvent::Finished { 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 { stage: OperationStage::SaveRegistry, 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) } -pub fn default_registry_path() -> PathBuf { - if let Some(path) = env::var_os("UPM_REGISTRY_PATH") { +fn registry_path() -> PathBuf { + if let Some(path) = env::var_os("AIM_REGISTRY_PATH") { return PathBuf::from(path); } let home = env::var_os("HOME").unwrap_or_else(|| ".".into()); - PathBuf::from(home).join(".local/share/upm/registry.toml") -} - -fn registry_path() -> PathBuf { - default_registry_path() + PathBuf::from(home).join(".local/share/aim/registry.toml") } #[derive(Debug, Eq, PartialEq)] @@ -210,49 +213,49 @@ pub enum DispatchResult { #[derive(Debug)] pub enum DispatchError { - AddPlan(upm_core::app::add::BuildAddPlanError), - AddInstall(upm_core::app::add::InstallAppError), + AddPlan(aim_core::app::add::BuildAddPlanError), + AddInstall(aim_core::app::add::InstallAppError), Prompt(ui::prompt::PromptError), - RemovePlan(upm_core::app::remove::RemoveRegisteredAppError), - Registry(upm_core::registry::store::RegistryStoreError), - Search(upm_core::app::search::SearchError), - Show(upm_core::domain::show::ShowResultError), - UpdatePlan(upm_core::app::update::BuildUpdatePlanError), - UpdateExecution(upm_core::app::update::ExecuteUpdatesError), + RemovePlan(aim_core::app::remove::RemoveRegisteredAppError), + Registry(aim_core::registry::store::RegistryStoreError), + Search(aim_core::app::search::SearchError), + Show(aim_core::domain::show::ShowResultError), + UpdatePlan(aim_core::app::update::BuildUpdatePlanError), + UpdateExecution(aim_core::app::update::ExecuteUpdatesError), } impl std::fmt::Display for DispatchError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::AddPlan(error) => match error { - upm_core::app::add::BuildAddPlanError::Query( - upm_core::app::query::ResolveQueryError::Unsupported, + aim_core::app::add::BuildAddPlanError::Query( + aim_core::app::query::ResolveQueryError::Unsupported, ) => write!(f, "unsupported source query"), - upm_core::app::add::BuildAddPlanError::InsecureHttpSource { .. } => write!( + aim_core::app::add::BuildAddPlanError::InsecureHttpSource { .. } => write!( f, "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, "no installable artifact found for {} {}", source.kind.as_str(), source.locator ), - upm_core::app::add::BuildAddPlanError::Adapter(id, error) => match error { - upm_core::adapters::traits::AdapterError::UnsupportedQuery => { + aim_core::app::add::BuildAddPlanError::Adapter(id, error) => match error { + aim_core::adapters::traits::AdapterError::UnsupportedQuery => { 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") } - upm_core::adapters::traits::AdapterError::ResolutionFailed(reason) => { + aim_core::adapters::traits::AdapterError::ResolutionFailed(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:?}") } - upm_core::app::add::BuildAddPlanError::NoCandidates => { + aim_core::app::add::BuildAddPlanError::NoCandidates => { 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::Search(error) => write!(f, "search failed: {error:?}"), Self::Show(error) => match error { - upm_core::domain::show::ShowResultError::AmbiguousInstalledMatch { + aim_core::domain::show::ShowResultError::AmbiguousInstalledMatch { query, matches, } => write!( @@ -270,14 +273,14 @@ impl std::fmt::Display for DispatchError { "multiple installed apps match {query}: {}", matches.join(", ") ), - upm_core::domain::show::ShowResultError::UnsupportedQuery => { + aim_core::domain::show::ShowResultError::UnsupportedQuery => { write!(f, "unsupported source query") } - upm_core::domain::show::ShowResultError::InsecureHttpSource => write!( + aim_core::domain::show::ShowResultError::InsecureHttpSource => write!( f, "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!( f, "no installable artifact found for {} {}", @@ -285,18 +288,18 @@ impl std::fmt::Display for DispatchError { source.locator ) } - upm_core::domain::show::ShowResultError::AdapterResolutionFailed { + aim_core::domain::show::ShowResultError::AdapterResolutionFailed { adapter_id, kind, detail, } => match kind { - upm_core::domain::show::AdapterFailureKind::UnsupportedQuery => { + aim_core::domain::show::AdapterFailureKind::UnsupportedQuery => { 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") } - upm_core::domain::show::AdapterFailureKind::ResolutionFailed => { + aim_core::domain::show::AdapterFailureKind::ResolutionFailed => { if let Some(detail) = detail { write!(f, "{adapter_id} resolution failed: {detail}") } else { @@ -304,27 +307,27 @@ impl std::fmt::Display for DispatchError { } } }, - upm_core::domain::show::ShowResultError::GitHubDiscoveryFailed { + aim_core::domain::show::ShowResultError::GitHubDiscoveryFailed { kind, detail, } => match (kind, detail) { ( - upm_core::domain::show::GitHubDiscoveryFailureKind::FixtureDocumentMissing, + aim_core::domain::show::GitHubDiscoveryFailureKind::FixtureDocumentMissing, Some(detail), ) => write!(f, "github discovery failed: missing fixture document {detail}"), ( - upm_core::domain::show::GitHubDiscoveryFailureKind::NoReleases, + aim_core::domain::show::GitHubDiscoveryFailureKind::NoReleases, Some(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") } - (upm_core::domain::show::GitHubDiscoveryFailureKind::Transport, _) => { + (aim_core::domain::show::GitHubDiscoveryFailureKind::Transport, _) => { write!(f, "github discovery failed: transport error") } _ => write!(f, "github discovery failed"), }, - upm_core::domain::show::ShowResultError::NoInstallableCandidates => { + aim_core::domain::show::ShowResultError::NoInstallableCandidates => { 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 { - upm_core::app::add::InstallAppError::Materialize(error) => format!("{error:?}"), - upm_core::app::add::InstallAppError::Policy(error) => error.clone(), - upm_core::app::add::InstallAppError::Download(error) => error.to_string(), - upm_core::app::add::InstallAppError::DownloadIo(error) => error.to_string(), - upm_core::app::add::InstallAppError::HostProbe(error) => error.to_string(), - upm_core::app::add::InstallAppError::Install(error) => error.to_string(), + aim_core::app::add::InstallAppError::Materialize(error) => format!("{error:?}"), + aim_core::app::add::InstallAppError::Policy(error) => error.clone(), + aim_core::app::add::InstallAppError::Download(error) => error.to_string(), + aim_core::app::add::InstallAppError::DownloadIo(error) => error.to_string(), + aim_core::app::add::InstallAppError::HostProbe(error) => error.to_string(), + aim_core::app::add::InstallAppError::Install(error) => error.to_string(), } } -impl From for DispatchError { - fn from(value: upm_core::app::add::BuildAddPlanError) -> Self { +impl From for DispatchError { + fn from(value: aim_core::app::add::BuildAddPlanError) -> Self { Self::AddPlan(value) } } -impl From for DispatchError { - fn from(value: upm_core::app::add::InstallAppError) -> Self { +impl From for DispatchError { + fn from(value: aim_core::app::add::InstallAppError) -> Self { Self::AddInstall(value) } } @@ -363,38 +366,38 @@ impl From for DispatchError { } } -impl From for DispatchError { - fn from(value: upm_core::app::update::BuildUpdatePlanError) -> Self { +impl From for DispatchError { + fn from(value: aim_core::app::update::BuildUpdatePlanError) -> Self { Self::UpdatePlan(value) } } -impl From for DispatchError { - fn from(value: upm_core::app::update::ExecuteUpdatesError) -> Self { +impl From for DispatchError { + fn from(value: aim_core::app::update::ExecuteUpdatesError) -> Self { Self::UpdateExecution(value) } } -impl From for DispatchError { - fn from(value: upm_core::app::remove::RemoveRegisteredAppError) -> Self { +impl From for DispatchError { + fn from(value: aim_core::app::remove::RemoveRegisteredAppError) -> Self { Self::RemovePlan(value) } } -impl From for DispatchError { - fn from(value: upm_core::registry::store::RegistryStoreError) -> Self { +impl From for DispatchError { + fn from(value: aim_core::registry::store::RegistryStoreError) -> Self { Self::Registry(value) } } -impl From for DispatchError { - fn from(value: upm_core::app::search::SearchError) -> Self { +impl From for DispatchError { + fn from(value: aim_core::app::search::SearchError) -> Self { Self::Search(value) } } -impl From for DispatchError { - fn from(value: upm_core::domain::show::ShowResultError) -> Self { +impl From for DispatchError { + fn from(value: aim_core::domain::show::ShowResultError) -> Self { Self::Show(value) } } @@ -439,7 +442,7 @@ fn merge_updated_app_records( } 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 .parent() .unwrap_or_else(|| Path::new(".")) @@ -451,7 +454,7 @@ fn install_home(registry_path: &Path) -> PathBuf { } 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(); return value == "1" || value.eq_ignore_ascii_case("true"); } diff --git a/crates/upm/src/main.rs b/crates/aim-cli/src/main.rs similarity index 53% rename from crates/upm/src/main.rs rename to crates/aim-cli/src/main.rs index cbb5f3e..eea6ffa 100644 --- a/crates/upm/src/main.rs +++ b/crates/aim-cli/src/main.rs @@ -1,16 +1,16 @@ fn main() { - let loaded_theme_config = upm::cli::config::AppConfig::load(); - upm::ui::theme::set_active_theme(upm::ui::theme::resolve_theme( + let loaded_theme_config = aim_cli::cli::config::AppConfig::load(); + aim_cli::ui::theme::set_active_theme(aim_cli::ui::theme::resolve_theme( &loaded_theme_config.config.theme, )); for warning in loaded_theme_config.warnings { 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, Err(error) => { eprintln!("{error}"); @@ -18,11 +18,11 @@ fn main() { } }; - let cli = upm::parse(); - let mut reporter = upm::ui::progress::TerminalProgressReporter::stderr(); - match upm::dispatch_with_reporter_and_config(cli, &config, &mut reporter) { + let cli = aim_cli::parse(); + let mut reporter = aim_cli::ui::progress::TerminalProgressReporter::stderr(); + match aim_cli::dispatch_with_reporter_and_config(cli, &config, &mut reporter) { Ok(result) => { - let output = upm::render_with_config(&result, &config); + let output = aim_cli::render_with_config(&result, &config); if !output.is_empty() { if reporter.emitted_output() { println!(); diff --git a/crates/upm/src/ui/mod.rs b/crates/aim-cli/src/ui/mod.rs similarity index 100% rename from crates/upm/src/ui/mod.rs rename to crates/aim-cli/src/ui/mod.rs diff --git a/crates/upm/src/ui/progress.rs b/crates/aim-cli/src/ui/progress.rs similarity index 98% rename from crates/upm/src/ui/progress.rs rename to crates/aim-cli/src/ui/progress.rs index 29e7489..6a42ea9 100644 --- a/crates/upm/src/ui/progress.rs +++ b/crates/aim-cli/src/ui/progress.rs @@ -1,8 +1,8 @@ use std::io::IsTerminal; use std::time::Duration; +use aim_core::app::progress::{OperationEvent, OperationKind, OperationStage, ProgressReporter}; use indicatif::{ProgressBar, ProgressStyle}; -use upm_core::app::progress::{OperationEvent, OperationKind, OperationStage, ProgressReporter}; pub fn new_progress_bar(total: Option) -> ProgressBar { match total { @@ -241,7 +241,7 @@ impl ProgressReporter for TerminalProgressReporter { mod tests { use super::TerminalProgressReporter; use crate::ui::progress::{ProgressReporter, format_completed_stage_line}; - use upm_core::app::progress::{OperationEvent, OperationStage}; + use aim_core::app::progress::{OperationEvent, OperationStage}; #[test] fn stage_change_resets_byte_progress_position() { diff --git a/crates/upm/src/ui/prompt.rs b/crates/aim-cli/src/ui/prompt.rs similarity index 94% rename from crates/upm/src/ui/prompt.rs rename to crates/aim-cli/src/ui/prompt.rs index 2bbc4cb..c3edf41 100644 --- a/crates/upm/src/ui/prompt.rs +++ b/crates/aim-cli/src/ui/prompt.rs @@ -1,11 +1,11 @@ use std::env; use std::io::IsTerminal; +use aim_core::app::add::{AddPlan, prefer_latest_tracking}; +use aim_core::app::interaction::{InteractionKind, InteractionRequest}; use dialoguer::Select; -use upm_core::app::add::{AddPlan, prefer_latest_tracking}; -use upm_core::app::interaction::{InteractionKind, InteractionRequest}; -const TRACKING_PREFERENCE_ENV: &str = "UPM_TRACKING_PREFERENCE"; +const TRACKING_PREFERENCE_ENV: &str = "AIM_TRACKING_PREFERENCE"; pub fn render_interaction(request: &InteractionRequest) -> String { match &request.kind { diff --git a/crates/upm/src/ui/render.rs b/crates/aim-cli/src/ui/render.rs similarity index 94% rename from crates/upm/src/ui/render.rs rename to crates/aim-cli/src/ui/render.rs index 47a5238..2581bb7 100644 --- a/crates/upm/src/ui/render.rs +++ b/crates/aim-cli/src/ui/render.rs @@ -1,10 +1,10 @@ -use console::measure_text_width; -use upm_core::app::add::AddPlan; -use upm_core::domain::search::SearchResults; -use upm_core::domain::show::{ +use aim_core::app::add::AddPlan; +use aim_core::domain::search::SearchResults; +use aim_core::domain::show::{ 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::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 { - upm_core::domain::app::InstallScope::User => "user", - upm_core::domain::app::InstallScope::System => "system", + aim_core::domain::app::InstallScope::User => "user", + aim_core::domain::app::InstallScope::System => "system", }; let warning_lines = added @@ -104,7 +104,7 @@ fn render_pending_add(plan: &AddPlan) -> String { .join("\n") } -fn render_list(rows: &[upm_core::app::list::ListRow]) -> String { +fn render_list(rows: &[aim_core::app::list::ListRow]) -> String { if rows.is_empty() { return crate::ui::theme::muted("No installed apps yet"); } @@ -170,7 +170,7 @@ fn format_list_row( } } -fn render_removed_app(removed: &upm_core::app::remove::RemovalResult) -> String { +fn render_removed_app(removed: &aim_core::app::remove::RemovalResult) -> String { let warning_lines = removed .warnings .iter() @@ -385,10 +385,10 @@ fn metadata_detail_lines(metadata: &MetadataSummary) -> Vec { lines } -fn installed_files_header(scope: Option) -> String { +fn installed_files_header(scope: Option) -> String { let label = match scope { - Some(upm_core::domain::app::InstallScope::User) => "Installed as User", - Some(upm_core::domain::app::InstallScope::System) => "Installed as System", + Some(aim_core::domain::app::InstallScope::User) => "Installed as User", + Some(aim_core::domain::app::InstallScope::System) => "Installed as System", None => "Installed files", }; @@ -422,11 +422,11 @@ fn truncate_checksum(checksum: &str) -> String { } } -fn metadata_kind_label(kind: upm_core::domain::update::ParsedMetadataKind) -> &'static str { +fn metadata_kind_label(kind: aim_core::domain::update::ParsedMetadataKind) -> &'static str { match kind { - upm_core::domain::update::ParsedMetadataKind::Unknown => "unknown", - upm_core::domain::update::ParsedMetadataKind::ElectronBuilder => "electron-builder", - upm_core::domain::update::ParsedMetadataKind::Zsync => "zsync", + aim_core::domain::update::ParsedMetadataKind::Unknown => "unknown", + aim_core::domain::update::ParsedMetadataKind::ElectronBuilder => "electron-builder", + aim_core::domain::update::ParsedMetadataKind::Zsync => "zsync", } } @@ -509,7 +509,7 @@ fn render_remote_show(remote: &RemoteShow) -> String { lines.join("\n") } -fn install_file_paths(added: &upm_core::app::add::InstalledApp) -> Vec { +fn install_file_paths(added: &aim_core::app::add::InstalledApp) -> Vec { [ Some( added @@ -595,7 +595,7 @@ fn render_search_results_with_config(results: &SearchResults, config: &CliConfig render_search_results(results) } -fn render_updated_apps(result: &upm_core::domain::update::UpdateExecutionResult) -> String { +fn render_updated_apps(result: &aim_core::domain::update::UpdateExecutionResult) -> String { let mut lines = vec![ crate::ui::theme::heading("Update Summary"), format!("updated apps: {}", result.updated_count()), @@ -621,7 +621,7 @@ fn render_updated_apps(result: &upm_core::domain::update::UpdateExecutionResult) 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)]; for item in &plan.items { diff --git a/crates/upm/src/ui/search_browser.rs b/crates/aim-cli/src/ui/search_browser.rs similarity index 99% rename from crates/upm/src/ui/search_browser.rs rename to crates/aim-cli/src/ui/search_browser.rs index 8a8616c..b518637 100644 --- a/crates/upm/src/ui/search_browser.rs +++ b/crates/aim-cli/src/ui/search_browser.rs @@ -2,6 +2,7 @@ use std::collections::BTreeSet; use std::io::IsTerminal; use std::time::Duration; +use aim_core::domain::search::{SearchInstallStatus, SearchResult, SearchResults}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::execute; use crossterm::terminal::{ @@ -13,7 +14,6 @@ use ratatui::style::Modifier; use ratatui::text::{Line, Span}; use ratatui::widgets::{Clear, List, ListItem, Paragraph, Wrap}; use ratatui::{Frame, Terminal}; -use upm_core::domain::search::{SearchInstallStatus, SearchResult, SearchResults}; use crate::config::{CliConfig, SearchConfig}; diff --git a/crates/upm/src/ui/theme.rs b/crates/aim-cli/src/ui/theme.rs similarity index 100% rename from crates/upm/src/ui/theme.rs rename to crates/aim-cli/src/ui/theme.rs diff --git a/crates/upm/tests/cli_commands.rs b/crates/aim-cli/tests/cli_commands.rs similarity index 78% rename from crates/upm/tests/cli_commands.rs rename to crates/aim-cli/tests/cli_commands.rs index aa6ad28..3842b51 100644 --- a/crates/upm/tests/cli_commands.rs +++ b/crates/aim-cli/tests/cli_commands.rs @@ -1,15 +1,15 @@ use assert_cmd::Command; use predicates::str::contains; +use aim_cli::cli::args::Command as AimCommand; +use aim_cli::{Cli, DispatchError}; +use aim_core::domain::show::{ShowResultError, SourceSummary}; +use aim_core::domain::source::SourceKind; use clap::Parser; -use upm::cli::args::Command as UpmCommand; -use upm::{Cli, DispatchError}; -use upm_core::domain::show::{ShowResultError, SourceSummary}; -use upm_core::domain::source::SourceKind; #[test] fn help_lists_expected_commands() { - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("--help") .assert() .success() @@ -22,20 +22,20 @@ fn help_lists_expected_commands() { #[test] fn cli_parses_show_subcommand() { - let cli = Cli::try_parse_from(["upm", "show", "legacy-bat"]).unwrap(); + let cli = Cli::try_parse_from(["aim", "show", "legacy-bat"]).unwrap(); 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:?}"), } } #[test] 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 { - Some(UpmCommand::Show { value }) => assert_eq!(value, None), + Some(AimCommand::Show { value }) => assert_eq!(value, None), other => panic!("expected bare show command, got {other:?}"), } } diff --git a/crates/upm/tests/cli_smoke.rs b/crates/aim-cli/tests/cli_smoke.rs similarity index 64% rename from crates/upm/tests/cli_smoke.rs rename to crates/aim-cli/tests/cli_smoke.rs index 0571d24..39d11be 100644 --- a/crates/upm/tests/cli_smoke.rs +++ b/crates/aim-cli/tests/cli_smoke.rs @@ -2,6 +2,6 @@ use assert_cmd::Command; #[test] 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(); } diff --git a/crates/aim-cli/tests/config_loading.rs b/crates/aim-cli/tests/config_loading.rs new file mode 100644 index 0000000..cbf13e7 --- /dev/null +++ b/crates/aim-cli/tests/config_loading.rs @@ -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:?}"), + } +} diff --git a/crates/upm/tests/end_to_end_cli.rs b/crates/aim-cli/tests/end_to_end_cli.rs similarity index 82% rename from crates/upm/tests/end_to_end_cli.rs rename to crates/aim-cli/tests/end_to_end_cli.rs index 2cc7aa3..8bdf7ab 100644 --- a/crates/upm/tests/end_to_end_cli.rs +++ b/crates/aim-cli/tests/end_to_end_cli.rs @@ -1,21 +1,21 @@ +use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; +use aim_core::registry::model::Registry; +use aim_core::registry::store::RegistryStore; use assert_cmd::Command; use predicates::prelude::PredicateBooleanExt; use predicates::str::contains; use tempfile::tempdir; -use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; -use upm_core::registry::model::Registry; -use upm_core::registry::store::RegistryStore; -const FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE"; +const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE"; #[test] fn list_command_runs_without_registry_entries() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("list") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .assert() .success() .stdout(contains("No installed apps yet")); @@ -31,10 +31,10 @@ fn list_command_reads_registered_apps_from_registry_file() { ) .unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("list") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .assert() .success() .stdout(contains("Name")) @@ -54,10 +54,10 @@ fn remove_command_removes_registered_app_from_registry_file() { ) .unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.args(["remove", "bat"]) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .assert() .success() .stdout(contains("Removed Bat")) @@ -73,14 +73,14 @@ fn remove_command_uninstalls_managed_files() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); let install_home = dir.path().join("install-home"); - let payload_path = install_home.join(".local/lib/upm/appimages/sharkdp-bat.AppImage"); - let desktop_path = install_home.join(".local/share/applications/upm-sharkdp-bat.desktop"); + let payload_path = install_home.join(".local/lib/aim/appimages/sharkdp-bat.AppImage"); + let desktop_path = install_home.join(".local/share/applications/aim-sharkdp-bat.desktop"); let 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 .arg("sharkdp/bat") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success(); @@ -89,10 +89,10 @@ fn remove_command_uninstalls_managed_files() { assert!(desktop_path.exists()); assert!(icon_path.exists()); - let mut remove_cmd = Command::cargo_bin("upm").unwrap(); + let mut remove_cmd = Command::cargo_bin("aim").unwrap(); remove_cmd .args(["remove", "sharkdp-bat"]) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .assert() .success() .stdout(contains("\nRemoved bat")) @@ -101,7 +101,7 @@ fn remove_command_uninstalls_managed_files() { .stdout(contains("Removed app:").not()) .stdout(contains("Removed files")) .stdout(contains("sharkdp-bat.AppImage")) - .stdout(contains("upm-sharkdp-bat.desktop")) + .stdout(contains("aim-sharkdp-bat.desktop")) .stdout(contains("sharkdp-bat.png")); assert!(!payload_path.exists()); @@ -113,10 +113,10 @@ fn remove_command_uninstalls_managed_files() { fn query_command_registers_unambiguous_app_in_registry_file() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("sharkdp/bat") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -140,10 +140,10 @@ fn query_command_registers_unambiguous_app_in_registry_file() { fn old_release_query_renders_tracking_prompt_without_writing_registry() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("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") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -158,12 +158,12 @@ fn old_release_query_renders_tracking_prompt_without_writing_registry() { fn old_release_query_can_track_latest_and_register_app() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("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") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") - .env("UPM_TRACKING_PREFERENCE", "latest") + .env("AIM_TRACKING_PREFERENCE", "latest") .assert() .success() .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\"")); } -#[test] -fn old_release_query_ignores_legacy_tracking_preference_env() { - let dir = tempdir().unwrap(); - let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); - - cmd.arg("https://github.com/pingdotgg/t3code/releases/download/v0.0.11/T3-Code-0.0.11-x86_64.AppImage") - .env("UPM_REGISTRY_PATH", ®istry_path) - .env(FIXTURE_MODE_ENV, "1") - .env("AIM_TRACKING_PREFERENCE", "latest") - .assert() - .success() - .stdout(contains("Choose update tracking")) - .stdout(contains("v0.0.11")) - .stdout(contains("v0.0.12")) - .stdout(contains("Installed t3code").not()); - - assert!(!registry_path.exists()); -} - #[test] fn cli_add_installs_and_renders_resolved_mode() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("sharkdp/bat") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -224,10 +204,10 @@ fn cli_add_installs_and_renders_resolved_mode() { fn positional_query_falls_back_to_search_for_plain_name_queries() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("firefox") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .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() { let dir = tempdir().unwrap(); 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") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .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() { let dir = tempdir().unwrap(); 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") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -293,10 +273,10 @@ fn cli_add_installs_appimagehub_source_with_truthful_origin() { fn cli_add_installs_gitlab_source_with_truthful_origin() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("https://gitlab.com/example/team-app") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -318,10 +298,10 @@ fn cli_add_preserves_direct_url_origin_for_provider_like_downloads() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); let query = "https://sourceforge.net/projects/team-app/files/team-app-1.0.0.AppImage/download"; - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg(query) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -341,10 +321,10 @@ fn cli_add_installs_sourceforge_latest_download_with_truthful_origin() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); let query = "https://sourceforge.net/projects/team-app/files/latest/download"; - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg(query) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -363,10 +343,10 @@ fn cli_add_installs_sourceforge_latest_download_with_truthful_origin() { fn cli_rejects_insecure_http_direct_urls_by_default() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("http://example.com/team-app.AppImage") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .assert() .failure() .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 config_path = dir.path().join("config.toml"); 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") - .env("UPM_REGISTRY_PATH", ®istry_path) - .env("UPM_CONFIG_PATH", &config_path) + .env("AIM_REGISTRY_PATH", ®istry_path) + .env("AIM_CONFIG_PATH", &config_path) .env(FIXTURE_MODE_ENV, "1") .assert() .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 config_path = dir.path().join("config.toml"); 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") - .env("UPM_REGISTRY_PATH", ®istry_path) - .env("UPM_CONFIG_PATH", &config_path) + .env("AIM_REGISTRY_PATH", ®istry_path) + .env("AIM_CONFIG_PATH", &config_path) .env(FIXTURE_MODE_ENV, "1") - .env("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP", "1") + .env("AIM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP", "1") .assert() .failure() .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() { let dir = tempdir().unwrap(); 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") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") - .env("UPM_APPIMAGEHUB_FIXTURE_BAD_MD5", "1") + .env("AIM_APPIMAGEHUB_FIXTURE_BAD_MD5", "1") .assert() .failure() .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 registry_path = dir.path().join("registry.toml"); let query = "https://sourceforge.net/projects/team-app/files/releases/beta/download"; - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg(query) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .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 query = "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) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .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() { let dir = tempdir().unwrap(); 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") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -499,10 +479,10 @@ fn cli_reports_unsupported_source_queries_distinctly() { fn cli_reports_supported_sources_without_installable_artifacts_distinctly() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("https://sourceforge.net/projects/team-app/") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -516,10 +496,10 @@ fn cli_reports_supported_sources_without_installable_artifacts_distinctly() { fn cli_add_emits_live_progress_to_stderr() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("sharkdp/bat") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -537,7 +517,7 @@ fn cli_add_emits_live_progress_to_stderr() { } #[test] -fn bare_upm_review_renders_review_heading() { +fn bare_aim_review_renders_review_heading() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); let store = RegistryStore::new(registry_path.clone()); @@ -562,9 +542,9 @@ fn bare_upm_review_renders_review_heading() { }) .unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); - cmd.env("UPM_REGISTRY_PATH", ®istry_path) + cmd.env("AIM_REGISTRY_PATH", ®istry_path) .assert() .success() .stdout(contains("Update Review")) @@ -581,10 +561,10 @@ fn remove_command_emits_live_progress_to_stderr() { ) .unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.args(["remove", "bat"]) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .assert() .success() .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"); 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"]) - .env("UPM_REGISTRY_PATH", ®istry_path) - .env("UPM_OS_RELEASE_PATH", &os_release_path) + .env("AIM_REGISTRY_PATH", ®istry_path) + .env("AIM_OS_RELEASE_PATH", &os_release_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -617,7 +597,7 @@ fn update_command_applies_updates() { let registry_path = dir.path().join("registry.toml"); let payload_path = dir .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()); store .save(&Registry { @@ -640,10 +620,10 @@ fn update_command_applies_updates() { }) .unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("update") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -683,10 +663,10 @@ fn update_command_emits_live_progress_to_stderr() { }) .unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("update") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -702,7 +682,7 @@ fn update_command_reports_when_previous_installation_is_restored() { let install_home = dir.path().join("install-home"); let store = RegistryStore::new(registry_path.clone()); let stable_id = "url-example.com-downloads-team-app.appimage"; - let payload_path = install_home.join(format!(".local/lib/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::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(), display_name: "https://example.com/downloads/team-app.AppImage".to_owned(), source_input: Some("https://example.com/downloads/team-app.AppImage".to_owned()), - source: Some(upm_core::domain::source::SourceRef { - kind: upm_core::domain::source::SourceKind::DirectUrl, + source: Some(aim_core::domain::source::SourceRef { + kind: aim_core::domain::source::SourceKind::DirectUrl, locator: "https://example.com/downloads/team-app.AppImage".to_owned(), - input_kind: upm_core::domain::source::SourceInputKind::DirectUrl, - normalized_kind: upm_core::domain::source::NormalizedSourceKind::DirectUrl, + input_kind: aim_core::domain::source::SourceInputKind::DirectUrl, + normalized_kind: aim_core::domain::source::NormalizedSourceKind::DirectUrl, canonical_locator: None, requested_tag: None, requested_asset_name: None, @@ -739,10 +719,10 @@ fn update_command_reports_when_previous_installation_is_restored() { }) .unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.arg("update") - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .env("DISPLAY", ":99") .env("XDG_CURRENT_DESKTOP", "test") diff --git a/crates/upm/tests/search_browser.rs b/crates/aim-cli/tests/search_browser.rs similarity index 93% rename from crates/upm/tests/search_browser.rs rename to crates/aim-cli/tests/search_browser.rs index 1a88b87..b355a3d 100644 --- a/crates/upm/tests/search_browser.rs +++ b/crates/aim-cli/tests/search_browser.rs @@ -1,6 +1,6 @@ -use upm::config::SearchConfig; -use upm::ui::search_browser::{BrowserPhase, SearchBrowserState, SubmitAction}; -use upm_core::domain::search::{SearchInstallStatus, SearchResult}; +use aim_cli::config::SearchConfig; +use aim_cli::ui::search_browser::{BrowserPhase, SearchBrowserState, SubmitAction}; +use aim_core::domain::search::{SearchInstallStatus, SearchResult}; #[test] fn browser_defaults_to_bottom_to_top_ordering() { @@ -81,7 +81,7 @@ fn invalid_numeric_input_keeps_last_good_selection_visible() { #[test] fn highlight_segments_marks_matching_query_fragments() { - let fragments = 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[1].text, "dotgg"); @@ -123,8 +123,8 @@ fn submit_selection_can_skip_confirmation_from_config() { assert_eq!( action, - SubmitAction::Confirmed(upm::ui::search_browser::SearchSelection { - rows: vec![upm::ui::search_browser::SearchRow { + SubmitAction::Confirmed(aim_cli::ui::search_browser::SearchSelection { + rows: vec![aim_cli::ui::search_browser::SearchRow { status: SearchInstallStatus::Available, provider_id: "github".to_owned(), display_name: "charlie/app".to_owned(), diff --git a/crates/upm/tests/search_cli.rs b/crates/aim-cli/tests/search_cli.rs similarity index 82% rename from crates/upm/tests/search_cli.rs rename to crates/aim-cli/tests/search_cli.rs index 5b0d977..8f0a1d2 100644 --- a/crates/upm/tests/search_cli.rs +++ b/crates/aim-cli/tests/search_cli.rs @@ -3,16 +3,16 @@ use predicates::prelude::PredicateBooleanExt; use predicates::str::contains; use tempfile::tempdir; -const FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE"; +const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE"; #[test] fn search_command_renders_remote_github_results() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.args(["search", "bat"]) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -46,10 +46,10 @@ fn search_command_renders_local_matches_in_deterministic_order() { ) .unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.args(["search", "bat"]) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -69,10 +69,10 @@ fn search_command_is_read_only_for_registry_contents() { let original = "version = 1\n[[apps]]\nstable_id = \"bat\"\ndisplay_name = \"Bat\"\n"; std::fs::write(®istry_path, original).unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.args(["search", "bat"]) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success(); @@ -88,11 +88,11 @@ fn search_command_fails_fast_on_malformed_config() { let config_path = dir.path().join("config.toml"); std::fs::write(&config_path, "[search\nskip_confirmation = true\n").unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.args(["search", "bat"]) - .env("UPM_REGISTRY_PATH", ®istry_path) - .env("UPM_CONFIG_PATH", &config_path) + .env("AIM_REGISTRY_PATH", ®istry_path) + .env("AIM_CONFIG_PATH", &config_path) .env(FIXTURE_MODE_ENV, "1") .assert() .failure() @@ -110,11 +110,11 @@ fn search_command_uses_plain_text_output_when_not_on_a_tty() { ) .unwrap(); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.args(["search", "bat"]) - .env("UPM_REGISTRY_PATH", ®istry_path) - .env("UPM_CONFIG_PATH", &config_path) + .env("AIM_REGISTRY_PATH", ®istry_path) + .env("AIM_CONFIG_PATH", &config_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -127,10 +127,10 @@ fn search_command_uses_plain_text_output_when_not_on_a_tty() { fn search_command_reports_loading_status_to_stderr() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.args(["search", "bat"]) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -141,10 +141,10 @@ fn search_command_reports_loading_status_to_stderr() { fn search_command_keeps_empty_results_in_plain_text_mode() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.args(["search", "no-such-app-image-query"]) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() @@ -156,10 +156,10 @@ fn search_command_keeps_empty_results_in_plain_text_mode() { fn search_command_renders_appimagehub_results() { let dir = tempdir().unwrap(); let registry_path = dir.path().join("registry.toml"); - let mut cmd = Command::cargo_bin("upm").unwrap(); + let mut cmd = Command::cargo_bin("aim").unwrap(); cmd.args(["search", "firefox"]) - .env("UPM_REGISTRY_PATH", ®istry_path) + .env("AIM_REGISTRY_PATH", ®istry_path) .env(FIXTURE_MODE_ENV, "1") .assert() .success() diff --git a/crates/upm/tests/ui_summary.rs b/crates/aim-cli/tests/ui_summary.rs similarity index 88% rename from crates/upm/tests/ui_summary.rs rename to crates/aim-cli/tests/ui_summary.rs index e51ff21..f7d1d3d 100644 --- a/crates/upm/tests/ui_summary.rs +++ b/crates/aim-cli/tests/ui_summary.rs @@ -1,28 +1,28 @@ -use upm::DispatchResult; -use upm::ui::prompt::render_interaction; -use upm::ui::render::{render_dispatch_result, render_update_summary}; -use upm::ui::search_browser::{SearchRow, format_search_row, render_confirmation_summary}; -use upm_core::app::add::InstalledApp; -use upm_core::app::interaction::{InteractionKind, InteractionRequest}; -use upm_core::app::list::ListRow; -use upm_core::app::remove::{RemovalPlan, RemovalResult}; -use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; -use upm_core::domain::search::SearchInstallStatus; -use upm_core::domain::show::{ +use aim_cli::DispatchResult; +use aim_cli::ui::prompt::render_interaction; +use aim_cli::ui::render::{render_dispatch_result, render_update_summary}; +use aim_cli::ui::search_browser::{SearchRow, format_search_row, render_confirmation_summary}; +use aim_core::app::add::InstalledApp; +use aim_core::app::interaction::{InteractionKind, InteractionRequest}; +use aim_core::app::list::ListRow; +use aim_core::app::remove::{RemovalPlan, RemovalResult}; +use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; +use aim_core::domain::search::SearchInstallStatus; +use aim_core::domain::show::{ InstalledShow, MetadataSummary, RemoteArtifactSummary, RemoteShow, ShowResult, SourceSummary, TrackedInstallPaths, UpdateChannelSummary, UpdateStrategySummary, }; -use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; -use upm_core::domain::update::ArtifactCandidate; -use upm_core::domain::update::{ +use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; +use aim_core::domain::update::ArtifactCandidate; +use aim_core::domain::update::{ ChannelPreference, ParsedMetadataKind, PlannedUpdate, UpdateChannelKind, UpdatePlan, }; -use upm_core::integration::install::InstallOutcome; +use aim_core::integration::install::InstallOutcome; 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; - upm::ui::theme::apply_style_spec(&format!("{title}:"), &style) + aim_cli::ui::theme::apply_style_spec(&format!("{title}:"), &style) } #[test] @@ -87,13 +87,13 @@ fn removal_summary_lists_removed_files() { stable_id: "bat".to_owned(), display_name: "Bat".to_owned(), artifact_paths: vec![ - "/tmp/install-home/.local/lib/upm/appimages/bat.AppImage".to_owned(), - "/tmp/install-home/.local/share/applications/upm-bat.desktop".to_owned(), + "/tmp/install-home/.local/lib/aim/appimages/bat.AppImage".to_owned(), + "/tmp/install-home/.local/share/applications/aim-bat.desktop".to_owned(), ], }, removed_paths: vec![ - "/tmp/install-home/.local/lib/upm/appimages/bat.AppImage".to_owned(), - "/tmp/install-home/.local/share/applications/upm-bat.desktop".to_owned(), + "/tmp/install-home/.local/lib/aim/appimages/bat.AppImage".to_owned(), + "/tmp/install-home/.local/share/applications/aim-bat.desktop".to_owned(), ], remaining_apps: Vec::new(), warnings: Vec::new(), @@ -101,7 +101,7 @@ fn removal_summary_lists_removed_files() { assert!(output.contains("Removed files")); assert!(output.contains("bat.AppImage")); - assert!(output.contains("upm-bat.desktop")); + assert!(output.contains("aim-bat.desktop")); } #[test] @@ -146,10 +146,10 @@ fn install_summary_omits_completed_steps_recap() { install: Some(InstallMetadata { scope: InstallScope::User, payload_path: Some( - "/tmp/install-home/.local/lib/upm/appimages/sharkdp-bat.AppImage".to_owned(), + "/tmp/install-home/.local/lib/aim/appimages/sharkdp-bat.AppImage".to_owned(), ), 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(), ), icon_path: None, @@ -176,12 +176,12 @@ fn install_summary_omits_completed_steps_recap() { tracks_latest: true, }, install_scope: InstallScope::User, - integration_mode: upm_core::integration::policy::IntegrationMode::Full, + integration_mode: aim_core::integration::policy::IntegrationMode::Full, 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(), 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, warnings: Vec::new(), @@ -285,8 +285,8 @@ fn installed_show_summary_renders_source_scope_and_paths() { install_scope: Some(InstallScope::User), tracked_paths: TrackedInstallPaths { payload_path: Some("/tmp/bat.AppImage".to_owned()), - desktop_entry_path: Some("/tmp/upm-bat.desktop".to_owned()), - icon_path: Some("/tmp/upm-bat.png".to_owned()), + desktop_entry_path: Some("/tmp/aim-bat.desktop".to_owned()), + icon_path: Some("/tmp/aim-bat.png".to_owned()), }, update_strategy: Some(UpdateStrategySummary { preferred: UpdateChannelSummary { @@ -325,30 +325,30 @@ fn installed_show_summary_renders_source_scope_and_paths() { assert!(output.contains(&format!( "{} {}", muted_bold_label("Source"), - upm::ui::theme::muted("github - sharkdp/bat") + aim_cli::ui::theme::muted("github - sharkdp/bat") ))); assert!(output.contains(&format!( "{} {}", muted_bold_label("Update Mechanism"), - upm::ui::theme::muted("electron-builder") + aim_cli::ui::theme::muted("electron-builder") ))); assert!(output.contains(&format!( "{} {}", muted_bold_label("Architecture"), - upm::ui::theme::muted("x86_64") + aim_cli::ui::theme::muted("x86_64") ))); assert!(output.contains(&format!( "{} {}", 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("/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("past version")); - assert!(!output.contains(&upm::ui::theme::label("Metadata"))); - assert!(!output.contains(&upm::ui::theme::label("Files"))); + assert!(!output.contains(&aim_cli::ui::theme::label("Metadata"))); + assert!(!output.contains(&aim_cli::ui::theme::label("Files"))); assert!(!output.contains("abcdefghijklmnopqrstuvwxyz0123456789")); } @@ -427,24 +427,24 @@ fn installed_show_summary_reports_when_newer_versions_are_available() { assert!(output.contains(&format!( "{} {}", muted_bold_label("Source"), - upm::ui::theme::muted("github - pingdotgg/t3code") + aim_cli::ui::theme::muted("github - pingdotgg/t3code") ))); assert!(output.contains(&format!( "{} {}", muted_bold_label("Update Mechanism"), - upm::ui::theme::muted("electron-builder") + aim_cli::ui::theme::muted("electron-builder") ))); assert!(output.contains(&format!( "{} {}", 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("[update available] User")); assert!(!output.contains("past versions")); assert!(!output.contains("latest v0.0.16")); - assert!(!output.contains(&upm::ui::theme::label("Metadata"))); - assert!(!output.contains(&upm::ui::theme::label("Files"))); + assert!(!output.contains(&aim_cli::ui::theme::label("Metadata"))); + assert!(!output.contains(&aim_cli::ui::theme::label("Files"))); } #[test] @@ -463,7 +463,7 @@ fn installed_show_list_renders_each_app_using_singular_show_format() { install_scope: Some(InstallScope::User), tracked_paths: TrackedInstallPaths { payload_path: Some("/tmp/bat.AppImage".to_owned()), - desktop_entry_path: Some("/tmp/upm-bat.desktop".to_owned()), + desktop_entry_path: Some("/tmp/aim-bat.desktop".to_owned()), icon_path: None, }, update_strategy: None, @@ -512,12 +512,12 @@ fn installed_show_list_renders_each_app_using_singular_show_format() { assert!(output.contains(&format!( "{} {}", muted_bold_label("Source"), - upm::ui::theme::muted("github - sharkdp/bat") + aim_cli::ui::theme::muted("github - sharkdp/bat") ))); assert!(output.contains(&format!( "{} {}", muted_bold_label("Source"), - upm::ui::theme::muted("github - pingdotgg/t3code") + aim_cli::ui::theme::muted("github - pingdotgg/t3code") ))); } diff --git a/crates/upm-core/Cargo.toml b/crates/aim-core/Cargo.toml similarity index 77% rename from crates/upm-core/Cargo.toml rename to crates/aim-core/Cargo.toml index 39c59d7..73716b6 100644 --- a/crates/upm-core/Cargo.toml +++ b/crates/aim-core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "upm-core" +name = "aim-core" version.workspace = true edition.workspace = true license.workspace = true @@ -17,8 +17,6 @@ serde.workspace = true serde_yaml.workspace = true sha2.workspace = true toml.workspace = true -upm-appimage = { path = "../upm-appimage" } -upm-module-api = { path = "../upm-module-api" } [dev-dependencies] tempfile.workspace = true diff --git a/crates/upm-core/src/adapters/appimagehub.rs b/crates/aim-core/src/adapters/appimagehub.rs similarity index 100% rename from crates/upm-core/src/adapters/appimagehub.rs rename to crates/aim-core/src/adapters/appimagehub.rs diff --git a/crates/upm-core/src/adapters/direct_url.rs b/crates/aim-core/src/adapters/direct_url.rs similarity index 100% rename from crates/upm-core/src/adapters/direct_url.rs rename to crates/aim-core/src/adapters/direct_url.rs diff --git a/crates/upm-core/src/adapters/github.rs b/crates/aim-core/src/adapters/github.rs similarity index 100% rename from crates/upm-core/src/adapters/github.rs rename to crates/aim-core/src/adapters/github.rs diff --git a/crates/upm-core/src/adapters/gitlab.rs b/crates/aim-core/src/adapters/gitlab.rs similarity index 100% rename from crates/upm-core/src/adapters/gitlab.rs rename to crates/aim-core/src/adapters/gitlab.rs diff --git a/crates/upm-core/src/adapters/mod.rs b/crates/aim-core/src/adapters/mod.rs similarity index 75% rename from crates/upm-core/src/adapters/mod.rs rename to crates/aim-core/src/adapters/mod.rs index 15a0aba..2d25232 100644 --- a/crates/upm-core/src/adapters/mod.rs +++ b/crates/aim-core/src/adapters/mod.rs @@ -1,3 +1,4 @@ +pub mod appimagehub; pub mod direct_url; pub mod github; pub mod gitlab; @@ -10,7 +11,14 @@ use crate::adapters::traits::SourceAdapter; use crate::domain::source::SourceRef; 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(adapter: &A, source: &SourceRef) -> bool { diff --git a/crates/upm-core/src/adapters/sourceforge.rs b/crates/aim-core/src/adapters/sourceforge.rs similarity index 100% rename from crates/upm-core/src/adapters/sourceforge.rs rename to crates/aim-core/src/adapters/sourceforge.rs diff --git a/crates/upm-core/src/adapters/test_support.rs b/crates/aim-core/src/adapters/test_support.rs similarity index 100% rename from crates/upm-core/src/adapters/test_support.rs rename to crates/aim-core/src/adapters/test_support.rs diff --git a/crates/upm-module-api/src/adapters/traits.rs b/crates/aim-core/src/adapters/traits.rs similarity index 93% rename from crates/upm-module-api/src/adapters/traits.rs rename to crates/aim-core/src/adapters/traits.rs index 3f4c3d3..0a2c1b6 100644 --- a/crates/upm-module-api/src/adapters/traits.rs +++ b/crates/aim-core/src/adapters/traits.rs @@ -59,8 +59,7 @@ pub trait SourceAdapter { } fn supports_source(&self, source: &SourceRef) -> bool { - self.repository_source_kind() == Some(source.kind) - || self.exact_source_kind() == Some(source.kind) + crate::adapters::supports_source(self, source) } fn resolve_source(&self, source: &SourceRef) -> Result { diff --git a/crates/upm-core/src/adapters/zsync.rs b/crates/aim-core/src/adapters/zsync.rs similarity index 100% rename from crates/upm-core/src/adapters/zsync.rs rename to crates/aim-core/src/adapters/zsync.rs diff --git a/crates/upm-core/src/app/add.rs b/crates/aim-core/src/app/add.rs similarity index 87% rename from crates/upm-core/src/app/add.rs rename to crates/aim-core/src/app/add.rs index 69e4276..fcf517a 100644 --- a/crates/upm-core/src/app/add.rs +++ b/crates/aim-core/src/app/add.rs @@ -3,6 +3,7 @@ use std::fs::{self, File}; use std::io::Read; use std::path::{Path, PathBuf}; +use crate::adapters::appimagehub::AppImageHubAdapter; use crate::adapters::direct_url::DirectUrlAdapter; use crate::adapters::gitlab::GitLabAdapter; use crate::adapters::sourceforge::SourceForgeAdapter; @@ -13,7 +14,6 @@ use crate::app::interaction::{InteractionKind, InteractionRequest}; use crate::app::progress::{ NoopReporter, OperationEvent, OperationKind, OperationStage, ProgressReporter, }; -use crate::app::providers::{ExternalAddResolution, ProviderRegistry}; use crate::app::query::{ResolveQueryError, resolve_query}; use crate::app::scope::{ScopeOverride, resolve_install_scope_with_default}; use crate::domain::app::{AppRecord, InstallMetadata, InstallScope}; @@ -25,14 +25,14 @@ use crate::integration::install::{ use crate::integration::policy::{IntegrationMode, resolve_install_policy}; use crate::metadata::parse_document; use crate::platform::probe_live_host; +use crate::source::appimagehub::resolve_appimagehub_item; use crate::source::github::{ GitHubDiscoveryError, GitHubTransport, discover_github_candidates_with, http_client_policy, }; use crate::update::channels::build_channels; use crate::update::ranking::{rank_channels, select_artifact, to_preference}; -const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE"; -const FIXTURE_MODE_ENV: &str = "UPM_GITHUB_FIXTURE_MODE"; +const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE"; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct AddSecurityPolicy { @@ -42,12 +42,11 @@ pub struct AddSecurityPolicy { pub fn build_add_plan(query: &str) -> Result { let transport = crate::source::github::default_transport(); let mut reporter = NoopReporter; - build_add_plan_with_reporter_and_policy_and_registry( + build_add_plan_with_reporter_and_policy( query, transport.as_ref(), &mut reporter, AddSecurityPolicy::default(), - &ProviderRegistry::default(), ) } @@ -56,12 +55,11 @@ pub fn build_add_plan_with( transport: &T, ) -> Result { let mut reporter = NoopReporter; - build_add_plan_with_reporter_and_policy_and_registry( + build_add_plan_with_reporter_and_policy( query, transport, &mut reporter, AddSecurityPolicy::default(), - &ProviderRegistry::default(), ) } @@ -70,40 +68,11 @@ pub fn build_add_plan_with_reporter( transport: &T, reporter: &mut impl ProgressReporter, ) -> Result { - build_add_plan_with_reporter_and_policy_and_registry( + build_add_plan_with_reporter_and_policy( query, transport, reporter, AddSecurityPolicy::default(), - &ProviderRegistry::default(), - ) -} - -pub fn build_add_plan_with_registered_providers( - query: &str, - transport: &T, - providers: &ProviderRegistry<'_>, - policy: AddSecurityPolicy, -) -> Result { - let mut reporter = NoopReporter; - build_add_plan_with_reporter_and_policy_and_registry( - query, - transport, - &mut reporter, - policy, - providers, - ) -} - -pub fn build_add_plan_with_reporter_and_registered_providers( - query: &str, - transport: &T, - reporter: &mut impl ProgressReporter, - providers: &ProviderRegistry<'_>, - policy: AddSecurityPolicy, -) -> Result { - build_add_plan_with_reporter_and_policy_and_registry( - query, transport, reporter, policy, providers, ) } @@ -112,22 +81,6 @@ pub fn build_add_plan_with_reporter_and_policy( transport: &T, reporter: &mut impl ProgressReporter, policy: AddSecurityPolicy, -) -> Result { - build_add_plan_with_reporter_and_policy_and_registry( - query, - transport, - reporter, - policy, - &ProviderRegistry::default(), - ) -} - -fn build_add_plan_with_reporter_and_policy_and_registry( - query: &str, - transport: &T, - reporter: &mut impl ProgressReporter, - policy: AddSecurityPolicy, - providers: &ProviderRegistry<'_>, ) -> Result { reporter.report(&OperationEvent::StageChanged { stage: OperationStage::ResolveQuery, @@ -138,7 +91,8 @@ fn build_add_plan_with_reporter_and_policy_and_registry { reporter.report(&OperationEvent::StageChanged { stage: OperationStage::DiscoverRelease, @@ -194,7 +148,6 @@ fn build_add_plan_with_reporter_and_policy_and_registry { @@ -235,29 +188,59 @@ fn build_add_plan_with_reporter_and_policy_and_registry { reporter.report(&OperationEvent::StageChanged { stage: OperationStage::DiscoverRelease, message: "discovering release".to_owned(), }); - if let Some(external_resolution) = - resolve_registered_external_add_provider(&source, providers)? + let adapter = AppImageHubAdapter; + let resolution = match adapter + .resolve_source(&source) + .map_err(|error| BuildAddPlanError::Adapter("appimagehub", error))? { - reporter.report(&OperationEvent::StageChanged { - stage: OperationStage::SelectArtifact, - message: "selecting artifact".to_owned(), - }); - ( - external_resolution.resolution, - external_resolution.selected_artifact, - external_resolution.update_strategy, - external_resolution.display_name_hint, - ) - } else { - return Err(BuildAddPlanError::NoInstallableArtifact { source }); - } + AdapterResolveOutcome::Resolved(resolution) => resolution, + AdapterResolveOutcome::NoInstallableArtifact { source } => { + return Err(BuildAddPlanError::NoInstallableArtifact { source }); + } + }; + let resolved_item = resolve_appimagehub_item(&resolution.source) + .map_err(|error| { + BuildAddPlanError::Adapter( + "appimagehub", + crate::adapters::traits::AdapterError::ResolutionFailed(format!( + "{error:?}" + )), + ) + })? + .ok_or(BuildAddPlanError::NoInstallableArtifact { + source: resolution.source.clone(), + })?; + display_name_hint = Some(resolved_item.title.clone()); + + reporter.report(&OperationEvent::StageChanged { + stage: OperationStage::SelectArtifact, + message: "selecting artifact".to_owned(), + }); + let artifact = ArtifactCandidate { + url: resolved_item.download.url.clone(), + version: resolved_item.version.clone(), + arch: resolved_item.download.arch.clone(), + trusted_checksum: None, + weak_checksum_md5: resolved_item.download.md5sum.clone(), + selection_reason: "provider-release".to_owned(), + }; + let strategy = UpdateStrategy { + preferred: crate::domain::update::ChannelPreference { + kind: crate::domain::update::UpdateChannelKind::DirectAsset, + locator: resolved_item.download.url.clone(), + reason: "provider-release".to_owned(), + }, + alternates: Vec::new(), + }; + + (resolution, artifact, strategy) } SourceKind::DirectUrl => { reporter.report(&OperationEvent::StageChanged { @@ -291,7 +274,7 @@ fn build_add_plan_with_reporter_and_policy_and_registry { reporter.report(&OperationEvent::StageChanged { @@ -332,7 +315,7 @@ fn build_add_plan_with_reporter_and_policy_and_registry { reporter.report(&OperationEvent::StageChanged { @@ -362,7 +345,7 @@ fn build_add_plan_with_reporter_and_policy_and_registry, -) -> Result, BuildAddPlanError> { - for provider in &providers.external_add_providers { - match provider.resolve(source) { - Ok(Some(resolution)) => return Ok(Some(resolution)), - Ok(None) => continue, - Err(error) => return Err(BuildAddPlanError::Adapter(provider.id(), error)), - } - } - - Ok(None) -} - pub fn prefer_latest_tracking(mut plan: AddPlan) -> AddPlan { if let Some(index) = plan .update_strategy @@ -497,7 +465,7 @@ pub fn install_app_with_reporter( install_home, &policy .desktop_entry_root - .join(format!("upm-{}.desktop", record.stable_id)), + .join(format!("aim-{}.desktop", record.stable_id)), ); let icon_path = resolve_target_path( install_home, @@ -507,7 +475,7 @@ pub fn install_app_with_reporter( stage: OperationStage::DownloadArtifact, 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 artifact_size_bytes = download_artifact_to_staged_path_with_reporter( &plan.selected_artifact.url, @@ -662,9 +630,7 @@ fn download_artifact_to_staged_path_with_reporter( ) -> Result { let policy = http_client_policy(); - if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1") - || env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") - { + if env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") { let bytes = b"\x7fELFAppImage\x89PNG\r\n\x1a\nicondataIEND\xaeB`\x82"; return download_to_staged_path_with_retries(staged_payload_path, reporter, policy, || { Ok(( diff --git a/crates/upm-core/src/app/identity.rs b/crates/aim-core/src/app/identity.rs similarity index 100% rename from crates/upm-core/src/app/identity.rs rename to crates/aim-core/src/app/identity.rs diff --git a/crates/upm-core/src/app/interaction.rs b/crates/aim-core/src/app/interaction.rs similarity index 100% rename from crates/upm-core/src/app/interaction.rs rename to crates/aim-core/src/app/interaction.rs diff --git a/crates/upm-core/src/app/list.rs b/crates/aim-core/src/app/list.rs similarity index 100% rename from crates/upm-core/src/app/list.rs rename to crates/aim-core/src/app/list.rs diff --git a/crates/upm-core/src/app/mod.rs b/crates/aim-core/src/app/mod.rs similarity index 66% rename from crates/upm-core/src/app/mod.rs rename to crates/aim-core/src/app/mod.rs index c0618d9..2d46ab8 100644 --- a/crates/upm-core/src/app/mod.rs +++ b/crates/aim-core/src/app/mod.rs @@ -1,15 +1,11 @@ pub mod add; -pub mod application; pub mod identity; pub mod interaction; pub mod list; pub mod progress; -pub mod providers; pub mod query; pub mod remove; pub mod scope; pub mod search; pub mod show; pub mod update; - -pub use application::{UpmApp, UpmAppBuilder}; diff --git a/crates/upm-core/src/app/progress.rs b/crates/aim-core/src/app/progress.rs similarity index 100% rename from crates/upm-core/src/app/progress.rs rename to crates/aim-core/src/app/progress.rs diff --git a/crates/upm-core/src/app/query.rs b/crates/aim-core/src/app/query.rs similarity index 100% rename from crates/upm-core/src/app/query.rs rename to crates/aim-core/src/app/query.rs diff --git a/crates/upm-core/src/app/remove.rs b/crates/aim-core/src/app/remove.rs similarity index 100% rename from crates/upm-core/src/app/remove.rs rename to crates/aim-core/src/app/remove.rs diff --git a/crates/upm-core/src/app/scope.rs b/crates/aim-core/src/app/scope.rs similarity index 100% rename from crates/upm-core/src/app/scope.rs rename to crates/aim-core/src/app/scope.rs diff --git a/crates/upm-core/src/app/search.rs b/crates/aim-core/src/app/search.rs similarity index 73% rename from crates/upm-core/src/app/search.rs rename to crates/aim-core/src/app/search.rs index d79b203..fd396ea 100644 --- a/crates/upm-core/src/app/search.rs +++ b/crates/aim-core/src/app/search.rs @@ -1,15 +1,35 @@ -use crate::app::providers::ProviderRegistry; use crate::domain::app::AppRecord; use crate::domain::search::{ InstalledSearchMatch, SearchInstallStatus, SearchQuery, SearchResult, SearchResults, SearchWarning, }; +use crate::source::appimagehub::{ + AppImageHubSearchError, AppImageHubTransport, search_appimagehub_with, +}; use crate::source::github::{ GitHubSearchError, GitHubTransport, TransportRelease, default_transport, search_github_repositories_with, }; use std::collections::HashSet; -pub use upm_module_api::app::search::{SearchProvider, SearchProviderError}; + +pub trait SearchProvider { + fn search(&self, query: &SearchQuery) -> Result, 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)] pub enum SearchError { @@ -20,31 +40,17 @@ pub fn build_search_results( query: &SearchQuery, installed_apps: &[AppRecord], ) -> Result { - 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, installed_apps, - &ProviderRegistry::default(), + &[&github_provider, &appimagehub_provider], ) } -pub fn build_search_results_with_registered_providers( - query: &SearchQuery, - installed_apps: &[AppRecord], - providers: &ProviderRegistry<'_>, -) -> Result { - let github_transport = default_transport(); - let github_provider = GitHubSearchProvider::new(github_transport.as_ref()); - let mut resolved_providers = vec![&github_provider as &dyn SearchProvider]; - resolved_providers.extend( - providers - .search_providers - .iter() - .map(|provider| provider.as_ref() as &dyn SearchProvider), - ); - - build_search_results_with(query, installed_apps, &resolved_providers) -} - pub fn build_search_results_with( query: &SearchQuery, 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 SearchProvider for AppImageHubSearchProvider<'_, T> { + fn search(&self, query: &SearchQuery) -> Result, SearchProviderError> { + let hits = search_appimagehub_with(&query.text, query.remote_limit, self.transport) + .map_err(|error| { + SearchProviderError::new("appimagehub", &render_appimagehub_search_error(&error)) + })?; + + let normalized_query = normalize_lookup(&query.text); + let mut ranked_hits = hits + .into_iter() + .enumerate() + .map(|(index, hit)| { + ( + appimagehub_remote_match_rank( + &normalized_query, + &hit.name, + hit.summary.as_deref(), + ), + index, + hit, + ) + }) + .collect::>(); + + ranked_hits.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1))); + + Ok(ranked_hits + .into_iter() + .map(|(_, _, hit)| SearchResult { + provider_id: "appimagehub".to_owned(), + display_name: hit.name, + description: hit.summary, + source_locator: hit.detail_page, + install_query: format!("appimagehub/{}", hit.id), + canonical_locator: hit.id, + version: Some(hit.version), + install_status: SearchInstallStatus::Available, + }) + .collect()) + } +} + impl SearchProvider for GitHubSearchProvider<'_, T> { fn search(&self, query: &SearchQuery) -> Result, SearchProviderError> { let name_only_query = format!("{} in:name", query.text); @@ -334,3 +392,45 @@ fn render_github_search_error(error: &GitHubSearchError) -> String { GitHubSearchError::Transport(inner) => inner.to_string(), } } + +fn appimagehub_remote_match_rank(query: &str, name: &str, summary: Option<&str>) -> u8 { + let name = normalize_lookup(name); + let summary = summary.map(normalize_lookup); + + if name == query { + return 0; + } + + if name.starts_with(query) { + return 1; + } + + if name.contains(query) { + return 2; + } + + if summary + .as_deref() + .map(|summary| summary.starts_with(query)) + .unwrap_or(false) + { + return 3; + } + + if summary + .as_deref() + .map(|summary| summary.contains(query)) + .unwrap_or(false) + { + return 4; + } + + 5 +} + +fn render_appimagehub_search_error(error: &AppImageHubSearchError) -> String { + match error { + AppImageHubSearchError::Parse(inner) => inner.to_string(), + AppImageHubSearchError::Transport(inner) => inner.to_string(), + } +} diff --git a/crates/upm-core/src/app/show.rs b/crates/aim-core/src/app/show.rs similarity index 100% rename from crates/upm-core/src/app/show.rs rename to crates/aim-core/src/app/show.rs diff --git a/crates/upm-core/src/app/update.rs b/crates/aim-core/src/app/update.rs similarity index 99% rename from crates/upm-core/src/app/update.rs rename to crates/aim-core/src/app/update.rs index 4333f56..dfb5b48 100644 --- a/crates/upm-core/src/app/update.rs +++ b/crates/aim-core/src/app/update.rs @@ -291,7 +291,7 @@ fn stage_existing_installation( } let stage_dir = install_home - .join(".local/share/upm/rollback") + .join(".local/share/aim/rollback") .join(&app.stable_id); fs::create_dir_all(&stage_dir) .map_err(|error| format!("failed to create rollback staging directory: {error}"))?; diff --git a/crates/upm-core/src/domain/app.rs b/crates/aim-core/src/domain/app.rs similarity index 100% rename from crates/upm-core/src/domain/app.rs rename to crates/aim-core/src/domain/app.rs diff --git a/crates/upm-core/src/domain/mod.rs b/crates/aim-core/src/domain/mod.rs similarity index 100% rename from crates/upm-core/src/domain/mod.rs rename to crates/aim-core/src/domain/mod.rs diff --git a/crates/upm-module-api/src/domain/search.rs b/crates/aim-core/src/domain/search.rs similarity index 100% rename from crates/upm-module-api/src/domain/search.rs rename to crates/aim-core/src/domain/search.rs diff --git a/crates/upm-core/src/domain/show.rs b/crates/aim-core/src/domain/show.rs similarity index 100% rename from crates/upm-core/src/domain/show.rs rename to crates/aim-core/src/domain/show.rs diff --git a/crates/upm-module-api/src/domain/source.rs b/crates/aim-core/src/domain/source.rs similarity index 100% rename from crates/upm-module-api/src/domain/source.rs rename to crates/aim-core/src/domain/source.rs diff --git a/crates/upm-core/src/domain/update.rs b/crates/aim-core/src/domain/update.rs similarity index 68% rename from crates/upm-core/src/domain/update.rs rename to crates/aim-core/src/domain/update.rs index 4c1cf73..9ce0ddc 100644 --- a/crates/upm-core/src/domain/update.rs +++ b/crates/aim-core/src/domain/update.rs @@ -1,7 +1,4 @@ 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)] pub enum ParsedMetadataKind { @@ -34,6 +31,25 @@ pub struct ParsedMetadata { 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)] pub struct UpdateChannel { pub kind: UpdateChannelKind, @@ -50,6 +66,30 @@ pub struct UpdateChannel { 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, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ArtifactCandidate { + pub url: String, + pub version: String, + pub arch: Option, + pub trusted_checksum: Option, + pub weak_checksum_md5: Option, + pub selection_reason: String, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct UpdatePlan { pub items: Vec, diff --git a/crates/upm-core/src/integration/desktop.rs b/crates/aim-core/src/integration/desktop.rs similarity index 100% rename from crates/upm-core/src/integration/desktop.rs rename to crates/aim-core/src/integration/desktop.rs diff --git a/crates/upm-core/src/integration/install.rs b/crates/aim-core/src/integration/install.rs similarity index 100% rename from crates/upm-core/src/integration/install.rs rename to crates/aim-core/src/integration/install.rs diff --git a/crates/upm-core/src/integration/mod.rs b/crates/aim-core/src/integration/mod.rs similarity index 100% rename from crates/upm-core/src/integration/mod.rs rename to crates/aim-core/src/integration/mod.rs diff --git a/crates/upm-core/src/integration/paths.rs b/crates/aim-core/src/integration/paths.rs similarity index 95% rename from crates/upm-core/src/integration/paths.rs rename to crates/aim-core/src/integration/paths.rs index 2851079..8cd7c16 100644 --- a/crates/upm-core/src/integration/paths.rs +++ b/crates/aim-core/src/integration/paths.rs @@ -11,7 +11,7 @@ pub fn managed_appimage_path(home_dir: &Path, scope: InstallScope, app_id: &str) } pub fn desktop_entry_path(home_dir: &Path, scope: InstallScope, app_id: &str) -> PathBuf { - scope_applications_dir(home_dir, scope).join(format!("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 { diff --git a/crates/upm-core/src/integration/policy.rs b/crates/aim-core/src/integration/policy.rs similarity index 94% rename from crates/upm-core/src/integration/policy.rs rename to crates/aim-core/src/integration/policy.rs index 2fe80db..fb99531 100644 --- a/crates/upm-core/src/integration/policy.rs +++ b/crates/aim-core/src/integration/policy.rs @@ -37,7 +37,7 @@ pub fn resolve_install_policy( (DistroFamily::Immutable, InstallScope::System) if capabilities.is_immutable => { Ok(InstallPolicy { scope: InstallScope::User, - payload_root: PathBuf::from(".local/lib/upm/appimages"), + payload_root: PathBuf::from(".local/lib/aim/appimages"), desktop_entry_root: PathBuf::from(".local/share/applications"), icon_root: PathBuf::from(".local/share/icons/hicolor/256x256/apps"), integration_mode: IntegrationMode::Degraded, @@ -57,7 +57,7 @@ pub fn resolve_install_policy( }), _ => Ok(InstallPolicy { scope: InstallScope::User, - payload_root: PathBuf::from(".local/lib/upm/appimages"), + payload_root: PathBuf::from(".local/lib/aim/appimages"), desktop_entry_root: PathBuf::from(".local/share/applications"), icon_root: PathBuf::from(".local/share/icons/hicolor/256x256/apps"), integration_mode: if capabilities.has_desktop_session { diff --git a/crates/upm-core/src/integration/refresh.rs b/crates/aim-core/src/integration/refresh.rs similarity index 86% rename from crates/upm-core/src/integration/refresh.rs rename to crates/aim-core/src/integration/refresh.rs index c4ebc69..740373b 100644 --- a/crates/upm-core/src/integration/refresh.rs +++ b/crates/aim-core/src/integration/refresh.rs @@ -62,7 +62,7 @@ fn icon_theme_root(icon_path: &Path) -> PathBuf { } fn audit_helper(helper: &Path, args: &[&Path]) { - if env::var("UPM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") { + if env::var("AIM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") { return; } @@ -72,7 +72,7 @@ fn audit_helper(helper: &Path, args: &[&Path]) { .collect::>() .join(" "); eprintln!( - "[upm] helper exec: {}{}{}", + "[aim] helper exec: {}{}{}", helper.display(), if rendered_args.is_empty() { "" } else { " " }, rendered_args @@ -80,23 +80,23 @@ fn audit_helper(helper: &Path, args: &[&Path]) { } fn audit_helper_status(helper: &Path, code: Option) { - if env::var("UPM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") { + if env::var("AIM_DEBUG_EXTERNAL_HELPERS").ok().as_deref() != Some("1") { return; } match code { - Some(code) => eprintln!("[upm] helper exit: {} code={code}", helper.display()), + Some(code) => eprintln!("[aim] helper exit: {} code={code}", helper.display()), None => eprintln!( - "[upm] helper exit: {} terminated by signal", + "[aim] helper exit: {} terminated by signal", helper.display() ), } } 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; } - eprintln!("[upm] helper failure: {} error={error}", helper.display()); + eprintln!("[aim] helper failure: {} error={error}", helper.display()); } diff --git a/crates/upm-core/src/lib.rs b/crates/aim-core/src/lib.rs similarity index 54% rename from crates/upm-core/src/lib.rs rename to crates/aim-core/src/lib.rs index c95d7ff..4a32a3f 100644 --- a/crates/upm-core/src/lib.rs +++ b/crates/aim-core/src/lib.rs @@ -7,6 +7,3 @@ pub mod platform; pub mod registry; pub mod source; pub mod update; - -pub use app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry}; -pub use app::{UpmApp, UpmAppBuilder}; diff --git a/crates/upm-core/src/metadata/document.rs b/crates/aim-core/src/metadata/document.rs similarity index 100% rename from crates/upm-core/src/metadata/document.rs rename to crates/aim-core/src/metadata/document.rs diff --git a/crates/upm-core/src/metadata/electron_builder.rs b/crates/aim-core/src/metadata/electron_builder.rs similarity index 100% rename from crates/upm-core/src/metadata/electron_builder.rs rename to crates/aim-core/src/metadata/electron_builder.rs diff --git a/crates/upm-core/src/metadata/mod.rs b/crates/aim-core/src/metadata/mod.rs similarity index 100% rename from crates/upm-core/src/metadata/mod.rs rename to crates/aim-core/src/metadata/mod.rs diff --git a/crates/upm-core/src/metadata/parser.rs b/crates/aim-core/src/metadata/parser.rs similarity index 100% rename from crates/upm-core/src/metadata/parser.rs rename to crates/aim-core/src/metadata/parser.rs diff --git a/crates/upm-core/src/metadata/zsync.rs b/crates/aim-core/src/metadata/zsync.rs similarity index 100% rename from crates/upm-core/src/metadata/zsync.rs rename to crates/aim-core/src/metadata/zsync.rs diff --git a/crates/upm-core/src/platform/capabilities.rs b/crates/aim-core/src/platform/capabilities.rs similarity index 97% rename from crates/upm-core/src/platform/capabilities.rs rename to crates/aim-core/src/platform/capabilities.rs index bdaab79..238bfc0 100644 --- a/crates/upm-core/src/platform/capabilities.rs +++ b/crates/aim-core/src/platform/capabilities.rs @@ -71,7 +71,7 @@ fn is_writable_dir(path: &Path) -> bool { return false; } - let probe_path = path.join(".upm-write-test"); + let probe_path = path.join(".aim-write-test"); let result = OpenOptions::new() .create(true) .write(true) diff --git a/crates/upm-core/src/platform/distro.rs b/crates/aim-core/src/platform/distro.rs similarity index 100% rename from crates/upm-core/src/platform/distro.rs rename to crates/aim-core/src/platform/distro.rs diff --git a/crates/upm-core/src/platform/mod.rs b/crates/aim-core/src/platform/mod.rs similarity index 93% rename from crates/upm-core/src/platform/mod.rs rename to crates/aim-core/src/platform/mod.rs index 4646924..4636245 100644 --- a/crates/upm-core/src/platform/mod.rs +++ b/crates/aim-core/src/platform/mod.rs @@ -10,11 +10,11 @@ pub use crate::domain::app::InstallScope; pub use capabilities::{DesktopHelpers, HostCapabilities, WritableRoots}; pub use distro::{DistroFamily, detect_distro_family}; -const OS_RELEASE_PATH_ENV: &str = "UPM_OS_RELEASE_PATH"; -const HELPER_PATHS_ENV: &str = "UPM_HELPER_PATHS"; +const OS_RELEASE_PATH_ENV: &str = "AIM_OS_RELEASE_PATH"; +const HELPER_PATHS_ENV: &str = "AIM_HELPER_PATHS"; 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 { @@ -26,7 +26,7 @@ pub fn user_icons_dir(home_dir: &Path) -> PathBuf { } pub fn system_managed_appimages_dir() -> PathBuf { - PathBuf::from("/opt/upm/appimages") + PathBuf::from("/opt/aim/appimages") } pub fn system_applications_dir() -> PathBuf { diff --git a/crates/upm-core/src/registry/mod.rs b/crates/aim-core/src/registry/mod.rs similarity index 100% rename from crates/upm-core/src/registry/mod.rs rename to crates/aim-core/src/registry/mod.rs diff --git a/crates/upm-core/src/registry/model.rs b/crates/aim-core/src/registry/model.rs similarity index 100% rename from crates/upm-core/src/registry/model.rs rename to crates/aim-core/src/registry/model.rs diff --git a/crates/upm-core/src/registry/store.rs b/crates/aim-core/src/registry/store.rs similarity index 100% rename from crates/upm-core/src/registry/store.rs rename to crates/aim-core/src/registry/store.rs diff --git a/crates/upm-core/src/source/appimagehub.rs b/crates/aim-core/src/source/appimagehub.rs similarity index 97% rename from crates/upm-core/src/source/appimagehub.rs rename to crates/aim-core/src/source/appimagehub.rs index 9b64dfc..c0db682 100644 --- a/crates/upm-core/src/source/appimagehub.rs +++ b/crates/aim-core/src/source/appimagehub.rs @@ -4,8 +4,7 @@ use std::time::Duration; use crate::domain::source::SourceRef; const DEFAULT_APPIMAGEHUB_API_BASE: &str = "https://api.appimagehub.com/ocs/v1/content"; -const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE"; -const FIXTURE_MODE_ENV: &str = "UPM_APPIMAGEHUB_FIXTURE_MODE"; +const FIXTURE_MODE_ENV: &str = "AIM_APPIMAGEHUB_FIXTURE_MODE"; #[derive(Clone, Debug, Eq, PartialEq)] pub struct AppImageHubDownload { @@ -57,8 +56,9 @@ pub trait AppImageHubTransport { } pub fn default_transport() -> Box { - if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1") - || env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") { + if env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") + || env::var("AIM_GITHUB_FIXTURE_MODE").ok().as_deref() == Some("1") + { Box::new(FixtureAppImageHubTransport) } else { Box::new(ReqwestAppImageHubTransport::new()) @@ -129,7 +129,7 @@ impl ReqwestAppImageHubTransport { .timeout(Duration::from_secs(30)) .build() .expect("reqwest client should build"), - api_base: env::var("UPM_APPIMAGEHUB_API_BASE") + api_base: env::var("AIM_APPIMAGEHUB_API_BASE") .unwrap_or_else(|_| DEFAULT_APPIMAGEHUB_API_BASE.to_owned()), } } @@ -424,11 +424,11 @@ fn resolved_version(item: &AppImageHubItem, download: &AppImageHubDownload) -> S } fn fixture_item(id: &str) -> Option { - let insecure_http = env::var("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP") + let insecure_http = env::var("AIM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP") .ok() .as_deref() == 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 { "2338455" => Some(AppImageHubItem { diff --git a/crates/upm-core/src/source/github.rs b/crates/aim-core/src/source/github.rs similarity index 97% rename from crates/upm-core/src/source/github.rs rename to crates/aim-core/src/source/github.rs index 193f497..dec32d4 100644 --- a/crates/upm-core/src/source/github.rs +++ b/crates/aim-core/src/source/github.rs @@ -5,8 +5,7 @@ use crate::domain::source::{ResolvedRelease, SourceRef}; use crate::metadata::MetadataDocument; const DEFAULT_GITHUB_API_BASE: &str = "https://api.github.com"; -const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE"; -const FIXTURE_MODE_ENV: &str = "UPM_GITHUB_FIXTURE_MODE"; +const FIXTURE_MODE_ENV: &str = "AIM_GITHUB_FIXTURE_MODE"; const DEFAULT_HTTP_TIMEOUT_SECS: u64 = 30; const DEFAULT_HTTP_MAX_RETRIES: usize = 3; @@ -177,9 +176,7 @@ pub fn search_github_repositories_with( } pub fn default_transport() -> Box { - if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1") - || env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") - { + if env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") { Box::new(FixtureGitHubTransport) } else { Box::new(ReqwestGitHubTransport::new()) @@ -203,13 +200,13 @@ impl ReqwestGitHubTransport { let mut default_headers = reqwest::header::HeaderMap::new(); default_headers.insert( reqwest::header::USER_AGENT, - reqwest::header::HeaderValue::from_static("upm/0.1"), + reqwest::header::HeaderValue::from_static("aim/0.1"), ); default_headers.insert( reqwest::header::ACCEPT, 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() .or_else(|| env::var("GITHUB_TOKEN").ok()) && let Ok(value) = reqwest::header::HeaderValue::from_str(&format!("Bearer {token}")) @@ -223,7 +220,7 @@ impl ReqwestGitHubTransport { .timeout(policy.timeout) .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()), } } diff --git a/crates/upm-core/src/source/input.rs b/crates/aim-core/src/source/input.rs similarity index 100% rename from crates/upm-core/src/source/input.rs rename to crates/aim-core/src/source/input.rs diff --git a/crates/upm-core/src/source/mod.rs b/crates/aim-core/src/source/mod.rs similarity index 59% rename from crates/upm-core/src/source/mod.rs rename to crates/aim-core/src/source/mod.rs index 72aea24..0b41ca4 100644 --- a/crates/upm-core/src/source/mod.rs +++ b/crates/aim-core/src/source/mod.rs @@ -1,2 +1,3 @@ +pub mod appimagehub; pub mod github; pub mod input; diff --git a/crates/upm-core/src/update/channels.rs b/crates/aim-core/src/update/channels.rs similarity index 100% rename from crates/upm-core/src/update/channels.rs rename to crates/aim-core/src/update/channels.rs diff --git a/crates/upm-core/src/update/mod.rs b/crates/aim-core/src/update/mod.rs similarity index 100% rename from crates/upm-core/src/update/mod.rs rename to crates/aim-core/src/update/mod.rs diff --git a/crates/upm-core/src/update/ranking.rs b/crates/aim-core/src/update/ranking.rs similarity index 100% rename from crates/upm-core/src/update/ranking.rs rename to crates/aim-core/src/update/ranking.rs diff --git a/crates/upm-core/tests/adapter_contract.rs b/crates/aim-core/tests/adapter_contract.rs similarity index 86% rename from crates/upm-core/tests/adapter_contract.rs rename to crates/aim-core/tests/adapter_contract.rs index ad2b38c..f1f3e14 100644 --- a/crates/upm-core/tests/adapter_contract.rs +++ b/crates/aim-core/tests/adapter_contract.rs @@ -1,14 +1,16 @@ -use upm_core::adapters::direct_url::DirectUrlAdapter; -use upm_core::adapters::github::GitHubAdapter; -use upm_core::adapters::gitlab::GitLabAdapter; -use upm_core::adapters::sourceforge::SourceForgeAdapter; -use upm_core::adapters::traits::{ +use aim_core::adapters::appimagehub::AppImageHubAdapter; +use aim_core::adapters::direct_url::DirectUrlAdapter; +use aim_core::adapters::github::GitHubAdapter; +use aim_core::adapters::gitlab::GitLabAdapter; +use aim_core::adapters::sourceforge::SourceForgeAdapter; +use aim_core::adapters::traits::{ AdapterCapabilities, AdapterError, AdapterResolution, AdapterResolveOutcome, SourceAdapter, }; -use upm_core::app::query::resolve_query; -use upm_core::domain::source::{ +use aim_core::app::query::resolve_query; +use aim_core::domain::source::{ NormalizedSourceKind, ResolvedRelease, SourceInputKind, SourceKind, SourceRef, }; +use aim_core::source::appimagehub::FixtureAppImageHubTransport; struct FileArtifactAdapter; @@ -59,6 +61,60 @@ fn adapter_capabilities_can_report_exact_resolution_only() { assert!(!capabilities.supports_search); } +#[test] +fn appimagehub_adapter_reports_search_and_exact_resolution_capabilities() { + let adapter = AppImageHubAdapter; + + assert_eq!(adapter.id(), "appimagehub"); + assert_eq!( + adapter.repository_source_kind(), + Some(SourceKind::AppImageHub) + ); + assert_eq!(adapter.exact_source_kind(), None); + assert_eq!( + adapter.capabilities(), + AdapterCapabilities { + supports_search: true, + supports_exact_resolution: true, + } + ); +} + +#[test] +fn appimagehub_adapter_resolves_installable_items_through_fixture_transport() { + let adapter = AppImageHubAdapter; + let source = resolve_query("appimagehub/2338455").unwrap(); + + let resolution = adapter + .resolve_source_with(&source, &FixtureAppImageHubTransport) + .unwrap(); + + assert!(matches!( + resolution, + AdapterResolveOutcome::Resolved(AdapterResolution { + source, + release: ResolvedRelease { version, .. }, + }) if source.kind == SourceKind::AppImageHub + && source.canonical_locator.as_deref() == Some("2338455") + && version == "latest" + )); +} + +#[test] +fn appimagehub_adapter_reports_no_installable_artifact_for_non_appimage_items() { + let adapter = AppImageHubAdapter; + let source = resolve_query("appimagehub/2337998").unwrap(); + + let resolution = adapter + .resolve_source_with(&source, &FixtureAppImageHubTransport) + .unwrap(); + + assert_eq!( + resolution, + AdapterResolveOutcome::NoInstallableArtifact { source } + ); +} + #[test] fn repository_backed_resolvers_accept_only_their_own_source_kind() { let github_source = resolve_query("sharkdp/bat").unwrap(); diff --git a/crates/upm-core/tests/adapter_smoke.rs b/crates/aim-core/tests/adapter_smoke.rs similarity index 79% rename from crates/upm-core/tests/adapter_smoke.rs rename to crates/aim-core/tests/adapter_smoke.rs index 40c2fd2..90dad47 100644 --- a/crates/upm-core/tests/adapter_smoke.rs +++ b/crates/aim-core/tests/adapter_smoke.rs @@ -1,14 +1,14 @@ -use upm_core::adapters::all_adapter_kinds; +use aim_core::adapters::all_adapter_kinds; #[test] fn all_expected_adapter_kinds_are_registered() { let kinds = all_adapter_kinds(); + assert!(kinds.contains(&"appimagehub")); assert!(kinds.contains(&"github")); assert!(kinds.contains(&"gitlab")); assert!(kinds.contains(&"direct-url")); assert!(kinds.contains(&"zsync")); assert!(kinds.contains(&"sourceforge")); - assert!(!kinds.contains(&"appimagehub")); assert!(!kinds.contains(&"custom-json")); } diff --git a/crates/upm-appimage/tests/appimagehub_search.rs b/crates/aim-core/tests/appimagehub_search.rs similarity index 58% rename from crates/upm-appimage/tests/appimagehub_search.rs rename to crates/aim-core/tests/appimagehub_search.rs index b06726a..7168e0d 100644 --- a/crates/upm-appimage/tests/appimagehub_search.rs +++ b/crates/aim-core/tests/appimagehub_search.rs @@ -1,16 +1,12 @@ -use upm_appimage::add::{AppImageHubAdapter, AppImageHubAddProvider}; -use upm_appimage::search::AppImageHubSearchProvider; -use upm_appimage::source::appimagehub::FixtureAppImageHubTransport; -use upm_core::adapters::traits::AdapterResolveOutcome; -use upm_core::app::providers::ExternalAddProvider; -use upm_core::app::query::resolve_query; -use upm_core::app::search::{ - GitHubSearchProvider, SearchProvider, SearchProviderError, build_search_results_with, +use aim_core::app::search::{ + AppImageHubSearchProvider, GitHubSearchProvider, SearchProvider, SearchProviderError, + build_search_results_with, }; -use upm_core::domain::app::AppRecord; -use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult}; -use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; -use upm_core::source::github::FixtureGitHubTransport; +use aim_core::domain::app::AppRecord; +use aim_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult}; +use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; +use aim_core::source::appimagehub::FixtureAppImageHubTransport; +use aim_core::source::github::FixtureGitHubTransport; struct StubProvider { hit: SearchResult, @@ -24,7 +20,7 @@ impl SearchProvider for StubProvider { #[test] 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(); @@ -38,7 +34,7 @@ fn appimagehub_search_provider_maps_hits_to_install_ready_results() { #[test] 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 { stable_id: "firefox".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] fn search_can_merge_github_and_appimagehub_providers() { let github = GitHubSearchProvider::new(&FixtureGitHubTransport); - let appimagehub = AppImageHubSearchProvider::new(Box::new(FixtureAppImageHubTransport)); + let appimagehub = AppImageHubSearchProvider::new(&FixtureAppImageHubTransport); let stub = StubProvider { hit: SearchResult { provider_id: "github".to_owned(), @@ -110,40 +106,3 @@ fn search_can_merge_github_and_appimagehub_providers() { .any(|hit| hit.provider_id == "appimagehub") ); } - -#[test] -fn appimagehub_adapter_resolves_installable_items_through_fixture_transport() { - let adapter = AppImageHubAdapter; - let source = resolve_query("appimagehub/2338455").unwrap(); - - let resolution = adapter - .resolve_source_with(&source, &FixtureAppImageHubTransport) - .unwrap(); - - assert!(matches!( - resolution, - AdapterResolveOutcome::Resolved(resolution) - if resolution.source.kind == SourceKind::AppImageHub - && resolution.source.canonical_locator.as_deref() == Some("2338455") - && resolution.release.version == "latest" - )); -} - -#[test] -fn appimagehub_add_provider_resolves_external_add_plan() { - let provider = AppImageHubAddProvider::new(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") - ); -} diff --git a/crates/upm-core/tests/checksum_verification.rs b/crates/aim-core/tests/checksum_verification.rs similarity index 97% rename from crates/upm-core/tests/checksum_verification.rs rename to crates/aim-core/tests/checksum_verification.rs index 08e48e5..b5ab8ce 100644 --- a/crates/upm-core/tests/checksum_verification.rs +++ b/crates/aim-core/tests/checksum_verification.rs @@ -1,8 +1,8 @@ use std::fs; +use aim_core::integration::install::{InstallRequest, PayloadInstallError, execute_install}; +use aim_core::platform::DesktopHelpers; use tempfile::tempdir; -use upm_core::integration::install::{InstallRequest, PayloadInstallError, execute_install}; -use upm_core::platform::DesktopHelpers; const VALID_FIXTURE_SHA512: &str = "ZZma4ZD+9XB4GGTHCNZu8I92OY02YrEvIG89ZtRNi99W8SZKwWkmGZz/QyNBxqAt0XeiKtcR80/dMnKlwpcIWw=="; diff --git a/crates/upm-core/tests/download_pipeline.rs b/crates/aim-core/tests/download_pipeline.rs similarity index 95% rename from crates/upm-core/tests/download_pipeline.rs rename to crates/aim-core/tests/download_pipeline.rs index e21d512..3b3eca4 100644 --- a/crates/upm-core/tests/download_pipeline.rs +++ b/crates/aim-core/tests/download_pipeline.rs @@ -2,15 +2,15 @@ use std::fs; use std::io::{self, Cursor, Read}; use std::time::Duration; -use tempfile::tempdir; -use upm_core::app::add::{ +use aim_core::app::add::{ InstallAppError, download_to_staged_path_with_retries, stream_payload_to_staged_file_with_reporter, }; -use upm_core::app::progress::{NoopReporter, OperationEvent}; -use upm_core::integration::install::{InstallRequest, execute_install}; -use upm_core::platform::DesktopHelpers; -use upm_core::source::github::HttpClientPolicy; +use aim_core::app::progress::{NoopReporter, OperationEvent}; +use aim_core::integration::install::{InstallRequest, execute_install}; +use aim_core::platform::DesktopHelpers; +use aim_core::source::github::HttpClientPolicy; +use tempfile::tempdir; #[test] fn payload_streaming_writes_staged_file_and_reports_progress() { diff --git a/crates/upm-core/tests/fixtures/example.zsync b/crates/aim-core/tests/fixtures/example.zsync similarity index 100% rename from crates/upm-core/tests/fixtures/example.zsync rename to crates/aim-core/tests/fixtures/example.zsync diff --git a/crates/upm-core/tests/fixtures/latest-linux.yml b/crates/aim-core/tests/fixtures/latest-linux.yml similarity index 100% rename from crates/upm-core/tests/fixtures/latest-linux.yml rename to crates/aim-core/tests/fixtures/latest-linux.yml diff --git a/crates/upm-core/tests/github_add_flow.rs b/crates/aim-core/tests/github_add_flow.rs similarity index 94% rename from crates/upm-core/tests/github_add_flow.rs rename to crates/aim-core/tests/github_add_flow.rs index 8dc3623..37da5c2 100644 --- a/crates/upm-core/tests/github_add_flow.rs +++ b/crates/aim-core/tests/github_add_flow.rs @@ -1,6 +1,6 @@ -use upm_core::app::add::{build_add_plan_with, materialize_app_record, prefer_latest_tracking}; -use upm_core::app::query::resolve_query; -use upm_core::source::github::FixtureGitHubTransport; +use aim_core::app::add::{build_add_plan_with, materialize_app_record, prefer_latest_tracking}; +use aim_core::app::query::resolve_query; +use aim_core::source::github::FixtureGitHubTransport; #[test] fn github_adapter_can_normalize_owner_repo_source() { diff --git a/crates/upm-core/tests/github_source_discovery.rs b/crates/aim-core/tests/github_source_discovery.rs similarity index 94% rename from crates/upm-core/tests/github_source_discovery.rs rename to crates/aim-core/tests/github_source_discovery.rs index 400b26f..7bc94ac 100644 --- a/crates/upm-core/tests/github_source_discovery.rs +++ b/crates/aim-core/tests/github_source_discovery.rs @@ -1,8 +1,8 @@ -use std::time::Duration; -use upm_core::app::query::resolve_query; -use upm_core::source::github::{ +use aim_core::app::query::resolve_query; +use aim_core::source::github::{ FixtureGitHubTransport, discover_github_candidates_with, http_client_policy, }; +use std::time::Duration; #[test] fn discovery_reports_appimage_assets_and_latest_linux_yml() { diff --git a/crates/upm-core/tests/identity_resolution.rs b/crates/aim-core/tests/identity_resolution.rs similarity index 86% rename from crates/upm-core/tests/identity_resolution.rs rename to crates/aim-core/tests/identity_resolution.rs index 564c93c..24e93cc 100644 --- a/crates/upm-core/tests/identity_resolution.rs +++ b/crates/aim-core/tests/identity_resolution.rs @@ -1,5 +1,5 @@ -use upm_core::app::identity::{IdentityFallback, resolve_identity}; -use upm_core::domain::app::IdentityConfidence; +use aim_core::app::identity::{IdentityFallback, resolve_identity}; +use aim_core::domain::app::IdentityConfidence; #[test] fn unresolved_identity_can_fall_back_to_url() { @@ -42,6 +42,6 @@ fn identifiers_containing_dot_dot_are_rejected() { assert_eq!( error, - upm_core::app::identity::ResolveIdentityError::InvalidStableId + aim_core::app::identity::ResolveIdentityError::InvalidStableId ); } diff --git a/crates/upm-core/tests/install_failures.rs b/crates/aim-core/tests/install_failures.rs similarity index 87% rename from crates/upm-core/tests/install_failures.rs rename to crates/aim-core/tests/install_failures.rs index bea996f..3f8e6ec 100644 --- a/crates/upm-core/tests/install_failures.rs +++ b/crates/aim-core/tests/install_failures.rs @@ -1,15 +1,15 @@ +use aim_core::app::add::{BuildAddPlanError, build_add_plan_with}; +use aim_core::app::query::ResolveQueryError; +use aim_core::app::update::execute_updates; +use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; +use aim_core::domain::source::SourceKind; +use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceRef}; +use aim_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install}; +use aim_core::platform::DesktopHelpers; +use aim_core::source::github::FixtureGitHubTransport; use std::fs; use std::sync::Mutex; use tempfile::tempdir; -use upm_core::app::add::{BuildAddPlanError, build_add_plan_with}; -use upm_core::app::query::ResolveQueryError; -use upm_core::app::update::execute_updates; -use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; -use upm_core::domain::source::SourceKind; -use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceRef}; -use upm_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install}; -use upm_core::platform::DesktopHelpers; -use upm_core::source::github::FixtureGitHubTransport; static ENV_LOCK: Mutex<()> = Mutex::new(()); @@ -27,7 +27,7 @@ fn integration_failure_removes_new_payload_and_generated_files() { fs::write(&staged_path, b"\x7fELFAppImage").unwrap(); let final_payload_path = payload_root.join("bat.AppImage"); - let desktop_entry_path = blocking_path.join("upm-bat.desktop"); + let desktop_entry_path = blocking_path.join("aim-bat.desktop"); let error = execute_install(&InstallRequest { staged_payload_path: &staged_path, final_payload_path: &final_payload_path, @@ -85,13 +85,13 @@ fn failed_update_restores_tracked_desktop_and_icon_files() { let root = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); std::env::set_var("DISPLAY", ":99"); std::env::set_var("XDG_CURRENT_DESKTOP", "test"); } let payload_path = root.path().join("tracked/team-app.AppImage"); - let desktop_path = root.path().join("tracked/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"); fs::create_dir_all(payload_path.parent().unwrap()).unwrap(); fs::write(&payload_path, b"previous-payload").unwrap(); diff --git a/crates/upm-core/tests/install_integration.rs b/crates/aim-core/tests/install_integration.rs similarity index 95% rename from crates/upm-core/tests/install_integration.rs rename to crates/aim-core/tests/install_integration.rs index 57eba49..8ed1ab1 100644 --- a/crates/upm-core/tests/install_integration.rs +++ b/crates/aim-core/tests/install_integration.rs @@ -1,13 +1,13 @@ +use aim_core::app::add::{build_add_plan_with_reporter, install_app_with_reporter}; +use aim_core::app::progress::{OperationEvent, OperationStage}; +use aim_core::domain::app::InstallScope; +use aim_core::domain::source::{NormalizedSourceKind, SourceKind}; +use aim_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install}; +use aim_core::platform::DesktopHelpers; +use aim_core::source::github::FixtureGitHubTransport; use std::fs; use std::os::unix::fs::PermissionsExt; use tempfile::tempdir; -use upm_core::app::add::{build_add_plan_with_reporter, install_app_with_reporter}; -use upm_core::app::progress::{OperationEvent, OperationStage}; -use upm_core::domain::app::InstallScope; -use upm_core::domain::source::{NormalizedSourceKind, SourceKind}; -use upm_core::integration::install::{DesktopIntegrationRequest, InstallRequest, execute_install}; -use upm_core::platform::DesktopHelpers; -use upm_core::source::github::FixtureGitHubTransport; fn write_staged_payload(root: &std::path::Path, name: &str, bytes: &[u8]) -> std::path::PathBuf { let staged_path = root.join("staging").join(format!("{name}.download")); @@ -32,7 +32,7 @@ fn install_writes_desktop_entry_and_reports_refresh_warning_only() { trusted_checksum: None, weak_checksum_md5: None, desktop: Some(DesktopIntegrationRequest { - desktop_entry_path: &desktop_root.join("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", icon_path: None, icon_bytes: None, @@ -86,7 +86,7 @@ fn install_executes_refresh_helpers_when_available() { trusted_checksum: None, weak_checksum_md5: None, desktop: Some(DesktopIntegrationRequest { - desktop_entry_path: &desktop_root.join("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", icon_path: Some(&icon_root.join("bat.png")), icon_bytes: None, @@ -128,7 +128,7 @@ fn install_extracts_icon_from_appimage_payload_when_icon_path_is_requested() { trusted_checksum: None, weak_checksum_md5: None, desktop: Some(DesktopIntegrationRequest { - desktop_entry_path: &desktop_root.join("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", icon_path: Some(&icon_root.join("bat.png")), icon_bytes: None, @@ -152,7 +152,7 @@ fn install_app_reports_operation_stages_in_order() { let mut events: Vec = Vec::new(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let mut reporter = |event: &OperationEvent| events.push(event.clone()); @@ -249,7 +249,7 @@ fn install_app_sanitizes_desktop_entry_display_names() { let mut reporter = Vec::new(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let mut capture = |event: &OperationEvent| reporter.push(event.clone()); @@ -349,7 +349,7 @@ fn gitlab_install_preserves_truthful_gitlab_origin() { let root = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let mut reporter = |_event: &OperationEvent| {}; @@ -399,7 +399,7 @@ fn direct_url_install_preserves_truthful_direct_url_origin() { let root = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let mut reporter = |_event: &OperationEvent| {}; @@ -484,7 +484,7 @@ fn sourceforge_latest_download_install_preserves_truthful_origin() { let root = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let mut reporter = |_event: &OperationEvent| {}; @@ -514,7 +514,7 @@ fn sourceforge_release_folder_install_preserves_truthful_origin() { let root = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let mut reporter = |_event: &OperationEvent| {}; @@ -577,7 +577,7 @@ fn sourceforge_file_like_release_download_install_preserves_input_but_stores_rel let root = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let mut reporter = |_event: &OperationEvent| {}; diff --git a/crates/aim-core/tests/install_paths.rs b/crates/aim-core/tests/install_paths.rs new file mode 100644 index 0000000..217e29e --- /dev/null +++ b/crates/aim-core/tests/install_paths.rs @@ -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")); +} diff --git a/crates/upm-core/tests/install_payload.rs b/crates/aim-core/tests/install_payload.rs similarity index 94% rename from crates/upm-core/tests/install_payload.rs rename to crates/aim-core/tests/install_payload.rs index 2e9225e..37f6278 100644 --- a/crates/upm-core/tests/install_payload.rs +++ b/crates/aim-core/tests/install_payload.rs @@ -1,7 +1,7 @@ +use aim_core::integration::install::stage_and_commit_payload; use std::fs; use std::os::unix::fs::PermissionsExt; use tempfile::tempdir; -use upm_core::integration::install::stage_and_commit_payload; #[test] fn payload_commit_moves_staged_appimage_into_final_location() { diff --git a/crates/upm-core/tests/install_policy.rs b/crates/aim-core/tests/install_policy.rs similarity index 87% rename from crates/upm-core/tests/install_policy.rs rename to crates/aim-core/tests/install_policy.rs index 026127c..272bb1f 100644 --- a/crates/upm-core/tests/install_policy.rs +++ b/crates/aim-core/tests/install_policy.rs @@ -1,6 +1,6 @@ +use aim_core::integration::policy::{IntegrationMode, resolve_install_policy}; +use aim_core::platform::{DistroFamily, HostCapabilities, InstallScope}; use std::path::Path; -use upm_core::integration::policy::{IntegrationMode, resolve_install_policy}; -use upm_core::platform::{DistroFamily, HostCapabilities, InstallScope}; #[test] fn immutable_system_request_downgrades_to_user_when_allowed() { @@ -36,7 +36,7 @@ fn system_policy_uses_managed_payload_and_native_integration_roots() { .unwrap(); assert_eq!(policy.scope, InstallScope::System); - assert_eq!(policy.payload_root, Path::new("/opt/upm/appimages")); + assert_eq!(policy.payload_root, Path::new("/opt/aim/appimages")); assert_eq!( policy.desktop_entry_root, Path::new("/usr/share/applications") diff --git a/crates/upm-core/tests/install_scope.rs b/crates/aim-core/tests/install_scope.rs similarity index 63% rename from crates/upm-core/tests/install_scope.rs rename to crates/aim-core/tests/install_scope.rs index b868999..c0411ce 100644 --- a/crates/upm-core/tests/install_scope.rs +++ b/crates/aim-core/tests/install_scope.rs @@ -1,5 +1,5 @@ -use upm_core::app::scope::{ScopeOverride, resolve_install_scope}; -use upm_core::domain::app::InstallScope; +use aim_core::app::scope::{ScopeOverride, resolve_install_scope}; +use aim_core::domain::app::InstallScope; #[test] fn explicit_scope_override_beats_effective_user() { diff --git a/crates/upm-core/tests/metadata_contract.rs b/crates/aim-core/tests/metadata_contract.rs similarity index 73% rename from crates/upm-core/tests/metadata_contract.rs rename to crates/aim-core/tests/metadata_contract.rs index 50bad24..24d0ba2 100644 --- a/crates/upm-core/tests/metadata_contract.rs +++ b/crates/aim-core/tests/metadata_contract.rs @@ -1,5 +1,5 @@ -use upm_core::domain::update::ParsedMetadataKind; -use upm_core::metadata::{MetadataDocument, parse_document}; +use aim_core::domain::update::ParsedMetadataKind; +use aim_core::metadata::{MetadataDocument, parse_document}; #[test] fn unknown_document_returns_typed_warning_not_panic() { diff --git a/crates/upm-core/tests/metadata_electron_builder.rs b/crates/aim-core/tests/metadata_electron_builder.rs similarity index 80% rename from crates/upm-core/tests/metadata_electron_builder.rs rename to crates/aim-core/tests/metadata_electron_builder.rs index d88e09f..cb9a474 100644 --- a/crates/upm-core/tests/metadata_electron_builder.rs +++ b/crates/aim-core/tests/metadata_electron_builder.rs @@ -1,5 +1,5 @@ -use upm_core::domain::update::ParsedMetadataKind; -use upm_core::metadata::{MetadataDocument, parse_document}; +use aim_core::domain::update::ParsedMetadataKind; +use aim_core::metadata::{MetadataDocument, parse_document}; #[test] fn parses_latest_linux_yml_into_download_hints() { diff --git a/crates/upm-core/tests/metadata_zsync.rs b/crates/aim-core/tests/metadata_zsync.rs similarity index 76% rename from crates/upm-core/tests/metadata_zsync.rs rename to crates/aim-core/tests/metadata_zsync.rs index d7079e2..c13fd4c 100644 --- a/crates/upm-core/tests/metadata_zsync.rs +++ b/crates/aim-core/tests/metadata_zsync.rs @@ -1,5 +1,5 @@ -use upm_core::domain::update::ParsedMetadataKind; -use upm_core::metadata::{MetadataDocument, parse_document}; +use aim_core::domain::update::ParsedMetadataKind; +use aim_core::metadata::{MetadataDocument, parse_document}; #[test] fn parses_zsync_document_into_channel_hints() { diff --git a/crates/upm-core/tests/platform_detection.rs b/crates/aim-core/tests/platform_detection.rs similarity index 93% rename from crates/upm-core/tests/platform_detection.rs rename to crates/aim-core/tests/platform_detection.rs index 726f852..424eeab 100644 --- a/crates/upm-core/tests/platform_detection.rs +++ b/crates/aim-core/tests/platform_detection.rs @@ -1,8 +1,8 @@ +use aim_core::platform::capabilities::{probe_desktop_helpers, probe_writable_roots}; +use aim_core::platform::distro::{DistroFamily, detect_distro_family}; use std::fs; use std::os::unix::fs::PermissionsExt; use tempfile::tempdir; -use upm_core::platform::capabilities::{probe_desktop_helpers, probe_writable_roots}; -use upm_core::platform::distro::{DistroFamily, detect_distro_family}; #[test] fn detects_fedora_family_from_os_release() { diff --git a/crates/upm-core/tests/query_resolution.rs b/crates/aim-core/tests/query_resolution.rs similarity index 94% rename from crates/upm-core/tests/query_resolution.rs rename to crates/aim-core/tests/query_resolution.rs index 8578ebd..4a24583 100644 --- a/crates/upm-core/tests/query_resolution.rs +++ b/crates/aim-core/tests/query_resolution.rs @@ -1,5 +1,5 @@ -use upm_core::app::query::resolve_query; -use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind}; +use aim_core::app::query::resolve_query; +use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind}; #[test] fn owner_repo_defaults_to_github() { @@ -233,21 +233,21 @@ fn classifies_single_segment_sourceforge_release_download_with_query_as_candidat fn rejects_malformed_gitlab_url() { let error = resolve_query("https://gitlab.com/example").unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] fn rejects_unsupported_gitlab_url_shape() { let error = resolve_query("https://gitlab.com/example/team-app/-/issues").unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] fn rejects_unsupported_gitlab_nested_resource_url() { let error = resolve_query("https://gitlab.com/example/team-app/issues").unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] @@ -255,14 +255,14 @@ fn rejects_unsupported_gitlab_release_permalink_url() { let error = resolve_query("https://gitlab.com/example/team-app/-/releases/permalink/latest") .unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] fn rejects_unsupported_gitlab_issue_detail_url() { let error = resolve_query("https://gitlab.com/example/team-app/issues/1").unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] @@ -270,7 +270,7 @@ fn rejects_unsupported_gitlab_blob_url() { let error = resolve_query("https://gitlab.com/example/team-app/blob/main/README.md").unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] @@ -291,28 +291,28 @@ fn classifies_ambiguous_gitlab_deep_reserved_segment_as_candidate() { fn rejects_unsupported_gitlab_packages_url() { let error = resolve_query("https://gitlab.com/example/team-app/packages").unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] fn rejects_malformed_sourceforge_url() { let error = resolve_query("https://sourceforge.net/projects/").unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] fn rejects_malformed_appimagehub_shorthand() { let error = resolve_query("appimagehub/firefox").unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] fn rejects_unsupported_sourceforge_url_shape() { let error = resolve_query("https://sourceforge.net/projects/team-app/rss").unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] @@ -335,7 +335,7 @@ fn rejects_unsupported_sourceforge_folder_download_shape() { let error = resolve_query("https://sourceforge.net/projects/team-app/files/releases/download") .unwrap_err(); - assert_eq!(error, upm_core::app::query::ResolveQueryError::Unsupported); + assert_eq!(error, aim_core::app::query::ResolveQueryError::Unsupported); } #[test] diff --git a/crates/upm-core/tests/registry_roundtrip.rs b/crates/aim-core/tests/registry_roundtrip.rs similarity index 78% rename from crates/upm-core/tests/registry_roundtrip.rs rename to crates/aim-core/tests/registry_roundtrip.rs index 11703b6..f0d5ec5 100644 --- a/crates/upm-core/tests/registry_roundtrip.rs +++ b/crates/aim-core/tests/registry_roundtrip.rs @@ -1,5 +1,5 @@ +use aim_core::registry::store::RegistryStore; use tempfile::tempdir; -use upm_core::registry::store::RegistryStore; #[test] fn registry_round_trips_app_records() { @@ -13,28 +13,28 @@ fn registry_round_trips_app_records() { fn registry_round_trips_update_strategy_and_alternates() { let dir = tempdir().unwrap(); let store = RegistryStore::new(dir.path().join("registry.toml")); - let registry = upm_core::registry::model::Registry { + let registry = aim_core::registry::model::Registry { version: 1, - apps: vec![upm_core::domain::app::AppRecord { + apps: vec![aim_core::domain::app::AppRecord { stable_id: "t3code".to_owned(), display_name: "T3 Code".to_owned(), source_input: Some("pingdotgg/t3code".to_owned()), source: None, installed_version: Some("0.0.11".to_owned()), - update_strategy: Some(upm_core::domain::update::UpdateStrategy { - preferred: upm_core::domain::update::ChannelPreference { - kind: upm_core::domain::update::UpdateChannelKind::DirectAsset, + update_strategy: Some(aim_core::domain::update::UpdateStrategy { + preferred: aim_core::domain::update::ChannelPreference { + kind: aim_core::domain::update::UpdateChannelKind::DirectAsset, locator: "https://example.test/app.AppImage".to_owned(), reason: "install-origin-match".to_owned(), }, alternates: vec![ - upm_core::domain::update::ChannelPreference { - kind: upm_core::domain::update::UpdateChannelKind::GitHubReleases, + aim_core::domain::update::ChannelPreference { + kind: aim_core::domain::update::UpdateChannelKind::GitHubReleases, locator: "pingdotgg/t3code".to_owned(), reason: "heuristic-match".to_owned(), }, - upm_core::domain::update::ChannelPreference { - kind: upm_core::domain::update::UpdateChannelKind::ElectronBuilder, + aim_core::domain::update::ChannelPreference { + kind: aim_core::domain::update::UpdateChannelKind::ElectronBuilder, locator: "https://example.test/latest-linux.yml".to_owned(), reason: "metadata-guided".to_owned(), }, @@ -57,9 +57,9 @@ fn registry_round_trips_update_strategy_and_alternates() { fn registry_round_trips_install_metadata() { let dir = tempdir().unwrap(); let store = RegistryStore::new(dir.path().join("registry.toml")); - let registry = upm_core::registry::model::Registry { + let registry = aim_core::registry::model::Registry { version: 1, - apps: vec![upm_core::domain::app::AppRecord { + apps: vec![aim_core::domain::app::AppRecord { stable_id: "t3code".to_owned(), display_name: "T3 Code".to_owned(), source_input: Some("pingdotgg/t3code".to_owned()), @@ -67,13 +67,13 @@ fn registry_round_trips_install_metadata() { installed_version: Some("0.0.11".to_owned()), update_strategy: None, metadata: Vec::new(), - install: Some(upm_core::domain::app::InstallMetadata { - scope: upm_core::domain::app::InstallScope::User, + install: Some(aim_core::domain::app::InstallMetadata { + scope: aim_core::domain::app::InstallScope::User, payload_path: Some( - "/tmp/install-home/.local/lib/upm/appimages/t3code.AppImage".to_owned(), + "/tmp/install-home/.local/lib/aim/appimages/t3code.AppImage".to_owned(), ), desktop_entry_path: Some( - "/tmp/install-home/.local/share/applications/upm-t3code.desktop".to_owned(), + "/tmp/install-home/.local/share/applications/aim-t3code.desktop".to_owned(), ), icon_path: Some( "/tmp/install-home/.local/share/icons/hicolor/256x256/apps/t3code.png" @@ -87,14 +87,14 @@ fn registry_round_trips_install_metadata() { let loaded = store.load().unwrap(); let install = loaded.apps[0].install.as_ref().unwrap(); - assert_eq!(install.scope, upm_core::domain::app::InstallScope::User); + assert_eq!(install.scope, aim_core::domain::app::InstallScope::User); assert_eq!( install.payload_path.as_deref(), - Some("/tmp/install-home/.local/lib/upm/appimages/t3code.AppImage") + Some("/tmp/install-home/.local/lib/aim/appimages/t3code.AppImage") ); assert_eq!( install.desktop_entry_path.as_deref(), - Some("/tmp/install-home/.local/share/applications/upm-t3code.desktop") + Some("/tmp/install-home/.local/share/applications/aim-t3code.desktop") ); assert_eq!( install.icon_path.as_deref(), @@ -106,18 +106,18 @@ fn registry_round_trips_install_metadata() { fn registry_round_trips_source_identity_for_new_provider_kinds() { let dir = tempdir().unwrap(); let store = RegistryStore::new(dir.path().join("registry.toml")); - let registry = upm_core::registry::model::Registry { + let registry = aim_core::registry::model::Registry { version: 1, apps: vec![ - upm_core::domain::app::AppRecord { + aim_core::domain::app::AppRecord { stable_id: "example-team-app".to_owned(), display_name: "team-app".to_owned(), source_input: Some("https://gitlab.com/example/team-app".to_owned()), - source: Some(upm_core::domain::source::SourceRef { - kind: upm_core::domain::source::SourceKind::GitLab, + source: Some(aim_core::domain::source::SourceRef { + kind: aim_core::domain::source::SourceKind::GitLab, locator: "https://gitlab.com/example/team-app".to_owned(), - input_kind: upm_core::domain::source::SourceInputKind::GitLabUrl, - normalized_kind: upm_core::domain::source::NormalizedSourceKind::GitLab, + input_kind: aim_core::domain::source::SourceInputKind::GitLabUrl, + normalized_kind: aim_core::domain::source::NormalizedSourceKind::GitLab, canonical_locator: Some("example/team-app".to_owned()), requested_tag: None, requested_asset_name: None, @@ -128,18 +128,18 @@ fn registry_round_trips_source_identity_for_new_provider_kinds() { metadata: Vec::new(), install: None, }, - upm_core::domain::app::AppRecord { + aim_core::domain::app::AppRecord { stable_id: "team-app".to_owned(), display_name: "team-app".to_owned(), source_input: Some( "https://sourceforge.net/projects/team-app/files/latest/download".to_owned(), ), - source: Some(upm_core::domain::source::SourceRef { - kind: upm_core::domain::source::SourceKind::SourceForge, + source: Some(aim_core::domain::source::SourceRef { + kind: aim_core::domain::source::SourceKind::SourceForge, locator: "https://sourceforge.net/projects/team-app/files/latest/download" .to_owned(), - input_kind: upm_core::domain::source::SourceInputKind::SourceForgeUrl, - normalized_kind: upm_core::domain::source::NormalizedSourceKind::SourceForge, + input_kind: aim_core::domain::source::SourceInputKind::SourceForgeUrl, + normalized_kind: aim_core::domain::source::NormalizedSourceKind::SourceForge, canonical_locator: Some("team-app".to_owned()), requested_tag: None, requested_asset_name: None, @@ -150,15 +150,15 @@ fn registry_round_trips_source_identity_for_new_provider_kinds() { metadata: Vec::new(), install: None, }, - upm_core::domain::app::AppRecord { + aim_core::domain::app::AppRecord { stable_id: "url-example.com-downloads-team-app.appimage".to_owned(), display_name: "https://example.com/downloads/team-app.AppImage".to_owned(), source_input: Some("https://example.com/downloads/team-app.AppImage".to_owned()), - source: Some(upm_core::domain::source::SourceRef { - kind: upm_core::domain::source::SourceKind::DirectUrl, + source: Some(aim_core::domain::source::SourceRef { + kind: aim_core::domain::source::SourceKind::DirectUrl, locator: "https://example.com/downloads/team-app.AppImage".to_owned(), - input_kind: upm_core::domain::source::SourceInputKind::DirectUrl, - normalized_kind: upm_core::domain::source::NormalizedSourceKind::DirectUrl, + input_kind: aim_core::domain::source::SourceInputKind::DirectUrl, + normalized_kind: aim_core::domain::source::NormalizedSourceKind::DirectUrl, canonical_locator: None, requested_tag: None, requested_asset_name: None, @@ -213,9 +213,9 @@ fn registry_save_is_atomic_and_cleans_up_temp_file() { let store = RegistryStore::new(registry_path.clone()); store - .save(&upm_core::registry::model::Registry { + .save(&aim_core::registry::model::Registry { version: 1, - apps: vec![upm_core::domain::app::AppRecord { + apps: vec![aim_core::domain::app::AppRecord { stable_id: "bat".to_owned(), display_name: "Bat".to_owned(), source_input: None, @@ -242,7 +242,7 @@ fn registry_exclusive_lock_rejects_second_mutator() { assert!(matches!( error, - upm_core::registry::store::RegistryStoreError::LockUnavailable + aim_core::registry::store::RegistryStoreError::LockUnavailable )); } @@ -251,9 +251,9 @@ fn registry_mutate_exclusive_reloads_and_writes_latest_state() { let dir = tempdir().unwrap(); let store = RegistryStore::new(dir.path().join("registry.toml")); store - .save(&upm_core::registry::model::Registry { + .save(&aim_core::registry::model::Registry { version: 1, - apps: vec![upm_core::domain::app::AppRecord { + apps: vec![aim_core::domain::app::AppRecord { stable_id: "bat".to_owned(), display_name: "Bat".to_owned(), source_input: None, @@ -268,7 +268,7 @@ fn registry_mutate_exclusive_reloads_and_writes_latest_state() { store .mutate_exclusive(|registry| { - registry.apps.push(upm_core::domain::app::AppRecord { + registry.apps.push(aim_core::domain::app::AppRecord { stable_id: "t3code".to_owned(), display_name: "T3 Code".to_owned(), source_input: None, diff --git a/crates/upm-core/tests/remove_flow.rs b/crates/aim-core/tests/remove_flow.rs similarity index 86% rename from crates/upm-core/tests/remove_flow.rs rename to crates/aim-core/tests/remove_flow.rs index fea6183..b15927a 100644 --- a/crates/upm-core/tests/remove_flow.rs +++ b/crates/aim-core/tests/remove_flow.rs @@ -1,13 +1,13 @@ -use std::path::Path; -use tempfile::tempdir; -use upm_core::app::interaction::{InteractionKind, InteractionRequest}; -use upm_core::app::list::build_list_rows; -use upm_core::app::progress::{OperationEvent, OperationStage}; -use upm_core::app::remove::{ +use aim_core::app::interaction::{InteractionKind, InteractionRequest}; +use aim_core::app::list::build_list_rows; +use aim_core::app::progress::{OperationEvent, OperationStage}; +use aim_core::app::remove::{ build_removal_plan, remove_registered_app_with_reporter, resolve_registered_app, }; -use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; -use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; +use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; +use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; +use std::path::Path; +use tempfile::tempdir; #[test] fn remove_flow_rejects_unknown_app_names() { @@ -74,7 +74,7 @@ fn ambiguous_remove_matches_include_stable_ids_for_client_choice() { assert_eq!( error, - upm_core::app::remove::ResolveRegisteredAppError::Ambiguous { + aim_core::app::remove::ResolveRegisteredAppError::Ambiguous { request: InteractionRequest { key: "select-registered-app".to_owned(), kind: InteractionKind::SelectRegisteredApp { @@ -98,8 +98,8 @@ fn removal_plan_prefers_persisted_install_metadata_paths() { metadata: Vec::new(), install: Some(InstallMetadata { scope: InstallScope::System, - payload_path: Some("/opt/upm/appimages/bat.AppImage".to_owned()), - desktop_entry_path: Some("/usr/share/applications/upm-bat.desktop".to_owned()), + payload_path: Some("/opt/aim/appimages/bat.AppImage".to_owned()), + desktop_entry_path: Some("/usr/share/applications/aim-bat.desktop".to_owned()), icon_path: Some("/usr/share/icons/hicolor/256x256/apps/bat.png".to_owned()), }), }; @@ -110,8 +110,8 @@ fn removal_plan_prefers_persisted_install_metadata_paths() { assert_eq!( plan.artifact_paths, vec![ - "/opt/upm/appimages/bat.AppImage".to_owned(), - "/usr/share/applications/upm-bat.desktop".to_owned(), + "/opt/aim/appimages/bat.AppImage".to_owned(), + "/usr/share/applications/aim-bat.desktop".to_owned(), "/usr/share/icons/hicolor/256x256/apps/bat.png".to_owned(), ] ); @@ -135,8 +135,8 @@ fn removal_plan_falls_back_to_derived_managed_user_paths() { assert_eq!( plan.artifact_paths, vec![ - "/home/test/.local/lib/upm/appimages/bat.AppImage".to_owned(), - "/home/test/.local/share/applications/upm-bat.desktop".to_owned(), + "/home/test/.local/lib/aim/appimages/bat.AppImage".to_owned(), + "/home/test/.local/share/applications/aim-bat.desktop".to_owned(), "/home/test/.local/share/icons/hicolor/256x256/apps/bat.png".to_owned(), ] ); @@ -158,7 +158,7 @@ fn remove_flow_reports_resolution_and_cleanup_events() { payload_path: Some( install_home .path() - .join(".local/lib/upm/appimages/bat.AppImage") + .join(".local/lib/aim/appimages/bat.AppImage") .display() .to_string(), ), diff --git a/crates/upm-core/tests/search_github.rs b/crates/aim-core/tests/search_github.rs similarity index 95% rename from crates/upm-core/tests/search_github.rs rename to crates/aim-core/tests/search_github.rs index e99f529..304c6f7 100644 --- a/crates/upm-core/tests/search_github.rs +++ b/crates/aim-core/tests/search_github.rs @@ -1,10 +1,10 @@ -use upm_core::app::search::{ +use aim_core::app::search::{ GitHubSearchProvider, SearchProvider, SearchProviderError, build_search_results_with, }; -use upm_core::domain::app::AppRecord; -use upm_core::domain::search::{SearchInstallStatus, SearchQuery}; -use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; -use upm_core::source::github::{FixtureGitHubTransport, search_github_repositories_with}; +use aim_core::domain::app::AppRecord; +use aim_core::domain::search::{SearchInstallStatus, SearchQuery}; +use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; +use aim_core::source::github::{FixtureGitHubTransport, search_github_repositories_with}; #[test] fn github_fixtures_return_normalized_remote_hits() { @@ -206,7 +206,7 @@ impl SearchProvider for FailingProvider { fn search( &self, _query: &SearchQuery, - ) -> Result, SearchProviderError> { + ) -> Result, SearchProviderError> { Err(SearchProviderError::new("github", "fixture rate limit")) } } diff --git a/crates/upm-core/tests/show_resolution.rs b/crates/aim-core/tests/show_resolution.rs similarity index 95% rename from crates/upm-core/tests/show_resolution.rs rename to crates/aim-core/tests/show_resolution.rs index 99853f2..d7bd78f 100644 --- a/crates/upm-core/tests/show_resolution.rs +++ b/crates/aim-core/tests/show_resolution.rs @@ -1,12 +1,12 @@ -use upm_core::app::show::{build_show_result, build_show_result_with}; -use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; -use upm_core::domain::show::{ShowResult, ShowResultError}; -use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; -use upm_core::domain::update::{ +use aim_core::app::show::{build_show_result, build_show_result_with}; +use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; +use aim_core::domain::show::{ShowResult, ShowResultError}; +use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; +use aim_core::domain::update::{ ChannelPreference, MetadataHints, ParsedMetadata, ParsedMetadataKind, UpdateChannelKind, UpdateStrategy, }; -use upm_core::source::github::FixtureGitHubTransport; +use aim_core::source::github::FixtureGitHubTransport; #[test] fn exact_installed_match_returns_installed_details() { @@ -48,8 +48,8 @@ fn exact_installed_match_returns_installed_details() { install: Some(InstallMetadata { scope: InstallScope::User, payload_path: Some("/tmp/bat.AppImage".to_owned()), - desktop_entry_path: Some("/tmp/upm-bat.desktop".to_owned()), - icon_path: Some("/tmp/upm-bat.png".to_owned()), + desktop_entry_path: Some("/tmp/aim-bat.desktop".to_owned()), + icon_path: Some("/tmp/aim-bat.png".to_owned()), }), }]; @@ -180,7 +180,7 @@ fn remote_show_projects_tracking_preference_interaction() { ShowResult::Remote(remote) => { assert!(remote.interactions.iter().any(|interaction| matches!( interaction, - upm_core::domain::show::RemoteInteractionSummary::ChooseTrackingPreference { .. } + aim_core::domain::show::RemoteInteractionSummary::ChooseTrackingPreference { .. } ))); } other => panic!("expected remote result, got {other:?}"), diff --git a/crates/upm-core/tests/update_planning.rs b/crates/aim-core/tests/update_planning.rs similarity index 95% rename from crates/upm-core/tests/update_planning.rs rename to crates/aim-core/tests/update_planning.rs index f5c391e..d45db1f 100644 --- a/crates/upm-core/tests/update_planning.rs +++ b/crates/aim-core/tests/update_planning.rs @@ -1,16 +1,16 @@ -use std::fs; -use std::sync::Mutex; -use tempfile::tempdir; -use upm_core::app::add::AddSecurityPolicy; -use upm_core::app::progress::{NoopReporter, OperationEvent, OperationStage}; -use upm_core::app::update::{ +use aim_core::app::add::AddSecurityPolicy; +use aim_core::app::progress::{NoopReporter, OperationEvent, OperationStage}; +use aim_core::app::update::{ build_update_plan, execute_updates, execute_updates_with_reporter, execute_updates_with_reporter_and_policy, }; -use upm_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; -use upm_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; -use upm_core::domain::update::{ChannelPreference, UpdateChannelKind, UpdateStrategy}; -use upm_core::integration::paths::managed_appimage_path; +use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope}; +use aim_core::domain::source::{NormalizedSourceKind, SourceInputKind, SourceKind, SourceRef}; +use aim_core::domain::update::{ChannelPreference, UpdateChannelKind, UpdateStrategy}; +use aim_core::integration::paths::managed_appimage_path; +use std::fs; +use std::sync::Mutex; +use tempfile::tempdir; static ENV_LOCK: Mutex<()> = Mutex::new(()); @@ -189,7 +189,7 @@ fn update_execution_rebuilds_gitlab_source_without_rewriting_origin() { let install_home = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let previous = AppRecord { @@ -253,7 +253,7 @@ fn update_execution_rebuilds_sourceforge_release_folder_without_rewriting_origin let install_home = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let previous = AppRecord { @@ -323,7 +323,7 @@ fn direct_http_updates_are_rejected_by_default() { let install_home = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let previous = AppRecord { @@ -357,7 +357,7 @@ fn direct_http_updates_are_rejected_by_default() { assert_eq!(result.failed_count(), 1); assert!(matches!( &result.items[0].status, - upm_core::domain::update::UpdateExecutionStatus::Failed { reason } + aim_core::domain::update::UpdateExecutionStatus::Failed { reason } if reason.contains("InsecureHttpSource") )); } @@ -370,7 +370,7 @@ fn direct_http_updates_can_be_allowed_by_policy() { let install_home = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let previous = AppRecord { @@ -417,7 +417,7 @@ fn update_execution_uses_stored_sourceforge_releases_root_for_file_like_inputs() let install_home = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); } let previous = AppRecord { @@ -482,7 +482,7 @@ fn failed_update_restores_previous_payload_contents() { let install_home = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); std::env::set_var("DISPLAY", ":99"); std::env::set_var("XDG_CURRENT_DESKTOP", "test"); } @@ -536,7 +536,7 @@ fn successful_update_removes_rollback_staging_directory() { let install_home = tempdir().unwrap(); unsafe { - std::env::set_var("UPM_GITHUB_FIXTURE_MODE", "1"); + std::env::set_var("AIM_GITHUB_FIXTURE_MODE", "1"); std::env::remove_var("DISPLAY"); std::env::remove_var("WAYLAND_DISPLAY"); std::env::remove_var("XDG_CURRENT_DESKTOP"); @@ -578,7 +578,7 @@ fn successful_update_removes_rollback_staging_directory() { assert!( !install_home .path() - .join(".local/share/upm/rollback") + .join(".local/share/aim/rollback") .exists() ); } diff --git a/crates/upm-appimage/Cargo.toml b/crates/upm-appimage/Cargo.toml deleted file mode 100644 index 2353d1c..0000000 --- a/crates/upm-appimage/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "upm-appimage" -version.workspace = true -edition.workspace = true -license.workspace = true - -[lib] -path = "src/lib.rs" - -[dependencies] -quick-xml.workspace = true -reqwest.workspace = true -serde.workspace = true -upm-module-api = { path = "../upm-module-api" } - -[dev-dependencies] -upm-core = { path = "../upm-core" } \ No newline at end of file diff --git a/crates/upm-appimage/src/add.rs b/crates/upm-appimage/src/add.rs deleted file mode 100644 index bb3258a..0000000 --- a/crates/upm-appimage/src/add.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::source::appimagehub::{ - AppImageHubError, AppImageHubTransport, resolve_appimagehub_item, resolve_appimagehub_item_with, -}; -use upm_module_api::adapters::traits::{ - AdapterCapabilities, AdapterError, AdapterResolution, AdapterResolveOutcome, SourceAdapter, -}; -use upm_module_api::app::providers::{ExternalAddProvider, ExternalAddResolution}; -use upm_module_api::domain::source::{ - NormalizedSourceKind, ResolvedRelease, SourceInputKind, SourceKind, SourceRef, -}; -use upm_module_api::domain::update::{ - ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy, -}; - -pub struct AppImageHubAdapter; - -impl AppImageHubAdapter { - pub fn resolve_source_with( - &self, - source: &SourceRef, - transport: &T, - ) -> Result { - if source.kind != SourceKind::AppImageHub { - return Err(AdapterError::UnsupportedSource); - } - - let resolved = resolve_appimagehub_item_with(source, transport) - .map_err(|error| AdapterError::ResolutionFailed(render_appimagehub_error(&error)))?; - - match resolved { - Some(item) => Ok(AdapterResolveOutcome::Resolved(AdapterResolution { - source: item.source, - release: ResolvedRelease { - version: item.version, - prerelease: false, - }, - })), - None => Ok(AdapterResolveOutcome::NoInstallableArtifact { - source: source.clone(), - }), - } - } -} - -impl SourceAdapter for AppImageHubAdapter { - fn id(&self) -> &'static str { - "appimagehub" - } - - fn capabilities(&self) -> AdapterCapabilities { - AdapterCapabilities { - supports_search: true, - supports_exact_resolution: true, - } - } - - fn repository_source_kind(&self) -> Option { - Some(SourceKind::AppImageHub) - } - - fn normalize(&self, query: &str) -> Result { - let source = resolve_appimagehub_query(query)?; - if source.kind != SourceKind::AppImageHub { - return Err(AdapterError::UnsupportedQuery); - } - - Ok(source) - } - - fn resolve(&self, source: &SourceRef) -> Result { - match resolve_appimagehub_item(source) - .map_err(|error| AdapterError::ResolutionFailed(render_appimagehub_error(&error)))? - { - Some(item) => Ok(AdapterResolution { - source: item.source, - release: ResolvedRelease { - version: item.version, - prerelease: false, - }, - }), - None => Err(AdapterError::ResolutionFailed( - "appimagehub item has no installable AppImage artifact".to_owned(), - )), - } - } - - fn resolve_supported_source( - &self, - source: &SourceRef, - ) -> Result { - let transport = crate::source::appimagehub::default_transport(); - self.resolve_source_with(source, transport.as_ref()) - } -} - -pub struct AppImageHubAddProvider { - transport: Box, -} - -impl AppImageHubAddProvider { - pub fn new(transport: Box) -> Self { - Self { transport } - } -} - -impl ExternalAddProvider for AppImageHubAddProvider { - fn id(&self) -> &'static str { - "appimagehub" - } - - fn resolve(&self, source: &SourceRef) -> Result, AdapterError> { - if source.kind != SourceKind::AppImageHub { - return Ok(None); - } - - let adapter = AppImageHubAdapter; - let resolution = match adapter.resolve_source_with(source, self.transport.as_ref())? { - AdapterResolveOutcome::Resolved(resolution) => resolution, - AdapterResolveOutcome::NoInstallableArtifact { .. } => return Ok(None), - }; - let Some(resolved_item) = resolve_appimagehub_item_with(source, self.transport.as_ref()) - .map_err(|error| AdapterError::ResolutionFailed(format!("{error:?}")))? - else { - return Ok(None); - }; - - Ok(Some(ExternalAddResolution { - resolution, - selected_artifact: ArtifactCandidate { - url: resolved_item.download.url.clone(), - version: resolved_item.version.clone(), - arch: resolved_item.download.arch.clone(), - trusted_checksum: None, - weak_checksum_md5: resolved_item.download.md5sum.clone(), - selection_reason: "provider-release".to_owned(), - }, - update_strategy: UpdateStrategy { - preferred: ChannelPreference { - kind: UpdateChannelKind::DirectAsset, - locator: resolved_item.download.url.clone(), - reason: "provider-release".to_owned(), - }, - alternates: Vec::new(), - }, - display_name_hint: Some(resolved_item.title), - })) - } -} - -fn render_appimagehub_error(error: &AppImageHubError) -> String { - match error { - AppImageHubError::FixtureItemMissing(id) => { - format!("missing appimagehub fixture item {id}") - } - AppImageHubError::InsecureDownloadUrl(url) => { - format!("insecure appimagehub download url: {url}") - } - AppImageHubError::Parse(error) => error.to_string(), - AppImageHubError::Transport(error) => error.to_string(), - AppImageHubError::UnsupportedSource(locator) => { - format!("unsupported appimagehub source: {locator}") - } - } -} - -fn resolve_appimagehub_query(query: &str) -> Result { - let trimmed = query.trim(); - let id = if let Some(id) = trimmed.strip_prefix("appimagehub/") { - id - } else if let Some(id) = trimmed.strip_prefix("https://www.appimagehub.com/p/") { - id - } else if let Some(id) = trimmed.strip_prefix("http://www.appimagehub.com/p/") { - id - } else { - return Err(AdapterError::UnsupportedQuery); - }; - - if !id.chars().all(|ch| ch.is_ascii_digit()) { - return Err(AdapterError::UnsupportedQuery); - } - - Ok(SourceRef { - kind: SourceKind::AppImageHub, - locator: format!("https://www.appimagehub.com/p/{id}"), - input_kind: if trimmed.starts_with("appimagehub/") { - SourceInputKind::AppImageHubShorthand - } else { - SourceInputKind::AppImageHubUrl - }, - normalized_kind: NormalizedSourceKind::AppImageHub, - canonical_locator: Some(id.to_owned()), - requested_tag: None, - requested_asset_name: None, - tracks_latest: true, - }) -} diff --git a/crates/upm-appimage/src/lib.rs b/crates/upm-appimage/src/lib.rs deleted file mode 100644 index a226e3f..0000000 --- a/crates/upm-appimage/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod add; -pub mod search; -pub mod source; - -pub use add::{AppImageHubAdapter, AppImageHubAddProvider}; -pub use search::AppImageHubSearchProvider; diff --git a/crates/upm-appimage/src/search.rs b/crates/upm-appimage/src/search.rs deleted file mode 100644 index b5c2361..0000000 --- a/crates/upm-appimage/src/search.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::source::appimagehub::{ - AppImageHubSearchError, AppImageHubTransport, search_appimagehub_with, -}; -use upm_module_api::app::search::{SearchProvider, SearchProviderError}; -use upm_module_api::domain::search::{SearchInstallStatus, SearchQuery, SearchResult}; - -pub struct AppImageHubSearchProvider { - transport: Box, -} - -impl AppImageHubSearchProvider { - pub fn new(transport: Box) -> Self { - Self { transport } - } -} - -impl SearchProvider for AppImageHubSearchProvider { - fn search(&self, query: &SearchQuery) -> Result, SearchProviderError> { - let hits = - search_appimagehub_with(&query.text, query.remote_limit, self.transport.as_ref()) - .map_err(|error| { - SearchProviderError::new( - "appimagehub", - &render_appimagehub_search_error(&error), - ) - })?; - - let normalized_query = normalize_lookup(&query.text); - let mut ranked_hits = hits - .into_iter() - .enumerate() - .map(|(index, hit)| { - ( - appimagehub_remote_match_rank( - &normalized_query, - &hit.name, - hit.summary.as_deref(), - ), - index, - hit, - ) - }) - .collect::>(); - - ranked_hits.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1))); - - Ok(ranked_hits - .into_iter() - .map(|(_, _, hit)| SearchResult { - provider_id: "appimagehub".to_owned(), - display_name: hit.name, - description: hit.summary, - source_locator: hit.detail_page, - install_query: format!("appimagehub/{}", hit.id), - canonical_locator: hit.id, - version: Some(hit.version), - install_status: SearchInstallStatus::Available, - }) - .collect()) - } -} - -fn normalize_lookup(value: &str) -> String { - value.trim().to_ascii_lowercase() -} - -fn appimagehub_remote_match_rank(query: &str, name: &str, summary: Option<&str>) -> u8 { - let name = normalize_lookup(name); - let summary = summary.map(normalize_lookup); - - if name == query { - return 0; - } - - if name.starts_with(query) { - return 1; - } - - if name.contains(query) { - return 2; - } - - if summary - .as_deref() - .map(|summary| summary.starts_with(query)) - .unwrap_or(false) - { - return 3; - } - - if summary - .as_deref() - .map(|summary| summary.contains(query)) - .unwrap_or(false) - { - return 4; - } - - 5 -} - -fn render_appimagehub_search_error(error: &AppImageHubSearchError) -> String { - match error { - AppImageHubSearchError::Parse(inner) => inner.to_string(), - AppImageHubSearchError::Transport(inner) => inner.to_string(), - } -} diff --git a/crates/upm-appimage/src/source/appimagehub.rs b/crates/upm-appimage/src/source/appimagehub.rs deleted file mode 100644 index d718478..0000000 --- a/crates/upm-appimage/src/source/appimagehub.rs +++ /dev/null @@ -1,517 +0,0 @@ -use std::env; -use std::time::Duration; - -use upm_module_api::domain::source::SourceRef; - -const DEFAULT_APPIMAGEHUB_API_BASE: &str = "https://api.appimagehub.com/ocs/v1/content"; -const GLOBAL_FIXTURE_MODE_ENV: &str = "UPM_FIXTURE_MODE"; -const FIXTURE_MODE_ENV: &str = "UPM_APPIMAGEHUB_FIXTURE_MODE"; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct AppImageHubDownload { - pub url: String, - pub name: String, - pub package_type: Option, - pub arch: Option, - pub md5sum: Option, - pub version: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct AppImageHubItem { - pub id: String, - pub name: String, - pub version: String, - pub summary: Option, - pub detail_page: String, - pub tags: Vec, - pub downloads: Vec, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct AppImageHubSearchHit { - pub id: String, - pub name: String, - pub version: String, - pub summary: Option, - pub detail_page: String, - pub tags: Vec, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ResolvedAppImageHubItem { - pub source: SourceRef, - pub title: String, - pub version: String, - pub download: AppImageHubDownload, -} - -pub trait AppImageHubTransport { - fn fetch_item(&self, id: &str) -> Result; - - fn search_items( - &self, - query: &str, - limit: usize, - ) -> Result, AppImageHubSearchError>; -} - -pub fn default_transport() -> Box { - if env::var(GLOBAL_FIXTURE_MODE_ENV).ok().as_deref() == Some("1") - || env::var(FIXTURE_MODE_ENV).ok().as_deref() == Some("1") - { - Box::new(FixtureAppImageHubTransport) - } else { - Box::new(ReqwestAppImageHubTransport::new()) - } -} - -pub fn resolve_appimagehub_item( - source: &SourceRef, -) -> Result, AppImageHubError> { - let transport = default_transport(); - resolve_appimagehub_item_with(source, transport.as_ref()) -} - -pub fn resolve_appimagehub_item_with( - source: &SourceRef, - transport: &T, -) -> Result, AppImageHubError> { - let item = transport.fetch_item(source_id(source)?)?; - let Some(download) = item - .downloads - .iter() - .find(|download| is_appimage_download(download)) - else { - return Ok(None); - }; - - validate_download_url(&download.url)?; - - Ok(Some(ResolvedAppImageHubItem { - source: source.clone(), - title: item.name.clone(), - version: resolved_version(&item, download), - download: download.clone(), - })) -} - -pub fn search_appimagehub( - query: &str, - limit: usize, -) -> Result, AppImageHubSearchError> { - let transport = default_transport(); - search_appimagehub_with(query, limit, transport.as_ref()) -} - -pub fn search_appimagehub_with( - query: &str, - limit: usize, - transport: &T, -) -> Result, AppImageHubSearchError> { - transport.search_items(query, limit) -} - -pub struct ReqwestAppImageHubTransport { - client: reqwest::blocking::Client, - api_base: String, -} - -impl Default for ReqwestAppImageHubTransport { - fn default() -> Self { - Self::new() - } -} - -impl ReqwestAppImageHubTransport { - pub fn new() -> Self { - Self { - client: reqwest::blocking::Client::builder() - .timeout(Duration::from_secs(30)) - .build() - .expect("reqwest client should build"), - api_base: env::var("UPM_APPIMAGEHUB_API_BASE") - .unwrap_or_else(|_| DEFAULT_APPIMAGEHUB_API_BASE.to_owned()), - } - } -} - -impl AppImageHubTransport for ReqwestAppImageHubTransport { - fn fetch_item(&self, id: &str) -> Result { - let url = format!("{}/data/{id}", self.api_base); - let xml = self - .client - .get(url) - .send() - .map_err(AppImageHubError::Transport)? - .error_for_status() - .map_err(AppImageHubError::Transport)? - .text() - .map_err(AppImageHubError::Transport)?; - - parse_item_xml(&xml) - } - - fn search_items( - &self, - query: &str, - limit: usize, - ) -> Result, AppImageHubSearchError> { - let url = format!("{}/data", self.api_base); - let xml = self - .client - .get(url) - .query(&[("search", query), ("pagesize", &limit.to_string())]) - .send() - .map_err(AppImageHubSearchError::Transport)? - .error_for_status() - .map_err(AppImageHubSearchError::Transport)? - .text() - .map_err(AppImageHubSearchError::Transport)?; - - parse_search_xml(&xml) - } -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct FixtureAppImageHubTransport; - -impl AppImageHubTransport for FixtureAppImageHubTransport { - fn fetch_item(&self, id: &str) -> Result { - fixture_item(id).ok_or_else(|| AppImageHubError::FixtureItemMissing(id.to_owned())) - } - - fn search_items( - &self, - query: &str, - limit: usize, - ) -> Result, AppImageHubSearchError> { - Ok(fixture_search_results(query, limit)) - } -} - -#[derive(Debug)] -pub enum AppImageHubError { - FixtureItemMissing(String), - InsecureDownloadUrl(String), - Parse(quick_xml::DeError), - Transport(reqwest::Error), - UnsupportedSource(String), -} - -#[derive(Debug)] -pub enum AppImageHubSearchError { - Parse(quick_xml::DeError), - Transport(reqwest::Error), -} - -#[derive(serde::Deserialize)] -struct OcsSingleResponse { - data: OcsSingleData, -} - -#[derive(serde::Deserialize)] -struct OcsSingleData { - content: OcsContent, -} - -#[derive(serde::Deserialize)] -struct OcsSearchResponse { - data: OcsSearchData, -} - -#[derive(serde::Deserialize)] -struct OcsSearchData { - #[serde(default)] - content: Vec, -} - -#[derive(serde::Deserialize)] -struct OcsContent { - id: String, - name: String, - version: Option, - summary: Option, - detailpage: Option, - tags: Option, - downloadlink1: Option, - downloadname1: Option, - download_package_type1: Option, - download_package_arch1: Option, - downloadmd5sum1: Option, - download_version1: Option, - downloadlink2: Option, - downloadname2: Option, - download_package_type2: Option, - download_package_arch2: Option, - downloadmd5sum2: Option, - download_version2: Option, - downloadlink3: Option, - downloadname3: Option, - download_package_type3: Option, - download_package_arch3: Option, - downloadmd5sum3: Option, - download_version3: Option, -} - -fn parse_item_xml(xml: &str) -> Result { - let parsed = - quick_xml::de::from_str::(xml).map_err(AppImageHubError::Parse)?; - Ok(content_to_item(parsed.data.content)) -} - -fn parse_search_xml(xml: &str) -> Result, AppImageHubSearchError> { - if !xml.contains("") { - return Ok(Vec::new()); - } - - let parsed = - quick_xml::de::from_str::(xml).map_err(AppImageHubSearchError::Parse)?; - Ok(parsed - .data - .content - .into_iter() - .map(|content| AppImageHubSearchHit { - id: content.id, - name: content.name, - version: normalize_version_text(content.version.as_deref()), - summary: content.summary, - detail_page: content - .detailpage - .unwrap_or_else(|| "https://www.appimagehub.com".to_owned()), - tags: split_tags(content.tags.as_deref()), - }) - .collect()) -} - -fn content_to_item(content: OcsContent) -> AppImageHubItem { - let detail_page = content - .detailpage - .clone() - .unwrap_or_else(|| "https://www.appimagehub.com".to_owned()); - let summary = content.summary.clone(); - let tags = split_tags(content.tags.as_deref()); - let downloads = collect_downloads(&content); - - AppImageHubItem { - id: content.id, - name: content.name, - version: normalize_version_text(content.version.as_deref()), - summary, - detail_page, - tags, - downloads, - } -} - -fn validate_download_url(url: &str) -> Result<(), AppImageHubError> { - if !url.starts_with("https://") { - return Err(AppImageHubError::InsecureDownloadUrl(url.to_owned())); - } - - Ok(()) -} - -fn collect_downloads(content: &OcsContent) -> Vec { - let mut downloads = Vec::new(); - - for download in [ - download_slot( - content.downloadlink1.as_deref(), - content.downloadname1.as_deref(), - content.download_package_type1.as_deref(), - content.download_package_arch1.as_deref(), - content.downloadmd5sum1.as_deref(), - content.download_version1.as_deref(), - ), - download_slot( - content.downloadlink2.as_deref(), - content.downloadname2.as_deref(), - content.download_package_type2.as_deref(), - content.download_package_arch2.as_deref(), - content.downloadmd5sum2.as_deref(), - content.download_version2.as_deref(), - ), - download_slot( - content.downloadlink3.as_deref(), - content.downloadname3.as_deref(), - content.download_package_type3.as_deref(), - content.download_package_arch3.as_deref(), - content.downloadmd5sum3.as_deref(), - content.download_version3.as_deref(), - ), - ] - .into_iter() - .flatten() - { - downloads.push(download); - } - - downloads -} - -fn download_slot( - link: Option<&str>, - name: Option<&str>, - package_type: Option<&str>, - arch: Option<&str>, - md5sum: Option<&str>, - version: Option<&str>, -) -> Option { - let url = link?.trim(); - if url.is_empty() { - return None; - } - - Some(AppImageHubDownload { - url: url.to_owned(), - name: name.unwrap_or("download").trim().to_owned(), - package_type: trim_optional(package_type), - arch: trim_optional(arch), - md5sum: trim_optional(md5sum), - version: trim_optional(version), - }) -} - -fn trim_optional(value: Option<&str>) -> Option { - value - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(ToOwned::to_owned) -} - -fn normalize_version_text(value: Option<&str>) -> String { - let value = value.map(str::trim).filter(|value| !value.is_empty()); - match value { - Some("Latest") | Some("latest") | None => "latest".to_owned(), - Some(other) => other.to_owned(), - } -} - -fn split_tags(tags: Option<&str>) -> Vec { - tags.unwrap_or_default() - .split(',') - .map(str::trim) - .filter(|tag| !tag.is_empty()) - .map(ToOwned::to_owned) - .collect() -} - -fn source_id(source: &SourceRef) -> Result<&str, AppImageHubError> { - source - .canonical_locator - .as_deref() - .or_else(|| source.locator.rsplit('/').next()) - .filter(|value| !value.is_empty()) - .ok_or_else(|| AppImageHubError::UnsupportedSource(source.locator.clone())) -} - -fn is_appimage_download(download: &AppImageHubDownload) -> bool { - download - .package_type - .as_deref() - .map(|kind| kind.eq_ignore_ascii_case("appimage")) - .unwrap_or(false) - || download.name.ends_with(".AppImage") -} - -fn resolved_version(item: &AppImageHubItem, download: &AppImageHubDownload) -> String { - download - .version - .as_deref() - .map(|value| normalize_version_text(Some(value))) - .filter(|value| value != "latest") - .unwrap_or_else(|| item.version.clone()) -} - -fn fixture_item(id: &str) -> Option { - let insecure_http = env::var("UPM_APPIMAGEHUB_FIXTURE_INSECURE_HTTP") - .ok() - .as_deref() - == Some("1"); - let bad_md5 = env::var("UPM_APPIMAGEHUB_FIXTURE_BAD_MD5").ok().as_deref() == Some("1"); - - match id { - "2338455" => Some(AppImageHubItem { - id: "2338455".to_owned(), - name: "Firefox by Mozilla - Official AppImage Edition".to_owned(), - version: "latest".to_owned(), - summary: Some("Take control of your internet with the Firefox browser".to_owned()), - detail_page: "https://www.appimagehub.com/p/2338455".to_owned(), - tags: vec![ - "appimage".to_owned(), - "x86-64".to_owned(), - "desktop".to_owned(), - "release-stable".to_owned(), - ], - downloads: vec![AppImageHubDownload { - url: if insecure_http { - "http://files06.pling.com/api/files/download/firefox-x86-64.AppImage".to_owned() - } else { - "https://files06.pling.com/api/files/download/firefox-x86-64.AppImage" - .to_owned() - }, - name: "firefox-x86-64.AppImage".to_owned(), - package_type: Some("appimage".to_owned()), - arch: Some("x86-64".to_owned()), - md5sum: Some(if bad_md5 { - "00000000000000000000000000000000".to_owned() - } else { - "2a685cf45213d5a2a243273fa68dafa6".to_owned() - }), - version: None, - }], - }), - "2337998" => Some(AppImageHubItem { - id: "2337998".to_owned(), - name: "Example Non-AppImage Package".to_owned(), - version: "latest".to_owned(), - summary: Some("An item that does not expose an AppImage download".to_owned()), - detail_page: "https://www.appimagehub.com/p/2337998".to_owned(), - tags: vec!["desktop".to_owned()], - downloads: vec![AppImageHubDownload { - url: "https://files06.pling.com/api/files/download/example.deb".to_owned(), - name: "example.deb".to_owned(), - package_type: Some("debian-package".to_owned()), - arch: Some("x86-64".to_owned()), - md5sum: None, - version: Some("2.1.1".to_owned()), - }], - }), - _ => None, - } -} - -fn fixture_search_results(query: &str, limit: usize) -> Vec { - let query = query.trim().to_ascii_lowercase(); - let fixtures = [ - AppImageHubSearchHit { - id: "2338455".to_owned(), - name: "Firefox by Mozilla - Official AppImage Edition".to_owned(), - version: "latest".to_owned(), - summary: Some("Take control of your internet with the Firefox browser".to_owned()), - detail_page: "https://www.appimagehub.com/p/2338455".to_owned(), - tags: vec!["browser".to_owned(), "appimage".to_owned()], - }, - AppImageHubSearchHit { - id: "2338484".to_owned(), - name: "Waterfox".to_owned(), - version: "latest".to_owned(), - summary: Some("Open Source, Private Browsing".to_owned()), - detail_page: "https://www.appimagehub.com/p/2338484".to_owned(), - tags: vec!["browser".to_owned(), "appimage".to_owned()], - }, - ]; - - fixtures - .into_iter() - .filter(|item| { - item.name.to_ascii_lowercase().contains(&query) - || item - .tags - .iter() - .any(|tag| tag.to_ascii_lowercase().contains(&query)) - }) - .take(limit) - .collect() -} diff --git a/crates/upm-appimage/src/source/mod.rs b/crates/upm-appimage/src/source/mod.rs deleted file mode 100644 index f23d1b0..0000000 --- a/crates/upm-appimage/src/source/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod appimagehub; diff --git a/crates/upm-core/src/adapters/traits.rs b/crates/upm-core/src/adapters/traits.rs deleted file mode 100644 index dc77862..0000000 --- a/crates/upm-core/src/adapters/traits.rs +++ /dev/null @@ -1 +0,0 @@ -pub use upm_module_api::adapters::traits::*; diff --git a/crates/upm-core/src/app/application.rs b/crates/upm-core/src/app/application.rs deleted file mode 100644 index f83e6e2..0000000 --- a/crates/upm-core/src/app/application.rs +++ /dev/null @@ -1,182 +0,0 @@ -use std::path::Path; - -use crate::app::add::{ - AddPlan, AddSecurityPolicy, BuildAddPlanError, InstalledApp, - build_add_plan_with_registered_providers, - build_add_plan_with_reporter_and_registered_providers, install_app_with_reporter, -}; -use crate::app::list::{ListRow, build_list_rows}; -use crate::app::progress::ProgressReporter; -use crate::app::providers::ProviderRegistry; -use crate::app::remove::{RemovalResult, RemoveRegisteredAppError, remove_registered_app}; -use crate::app::search::{SearchError, SearchProvider, build_search_results_with}; -use crate::app::show::build_show_result_with; -use crate::app::update::{ - BuildUpdatePlanError, ExecuteUpdatesError, build_update_plan, - execute_updates_with_reporter_and_policy, -}; -use crate::domain::app::{AppRecord, InstallScope}; -use crate::domain::search::{SearchQuery, SearchResults}; -use crate::domain::show::{InstalledShow, ShowResult, ShowResultError}; -use crate::domain::update::{UpdateExecutionResult, UpdatePlan}; -use crate::source::github::{GitHubTransport, default_transport}; - -pub struct UpmApp<'a> { - github_transport: Box, - providers: ProviderRegistry<'a>, -} - -pub struct UpmAppBuilder<'a> { - github_transport: Option>, - providers: ProviderRegistry<'a>, -} - -impl UpmApp<'static> { - pub fn new() -> Self { - Self::builder() - .with_provider_registry(default_provider_registry()) - .build() - } -} - -impl Default for UpmApp<'static> { - fn default() -> Self { - Self::new() - } -} - -impl<'a> UpmApp<'a> { - pub fn builder() -> UpmAppBuilder<'a> { - UpmAppBuilder { - github_transport: None, - providers: ProviderRegistry::default(), - } - } - - pub fn search( - &self, - query: &SearchQuery, - installed_apps: &[AppRecord], - ) -> Result { - let github_provider = - crate::app::search::GitHubSearchProvider::new(self.github_transport.as_ref()); - let mut resolved_providers = vec![&github_provider as &dyn SearchProvider]; - resolved_providers.extend( - self.providers - .search_providers - .iter() - .map(|provider| provider.as_ref() as &dyn SearchProvider), - ); - build_search_results_with(query, installed_apps, &resolved_providers) - } - - pub fn build_add_plan( - &self, - query: &str, - policy: AddSecurityPolicy, - ) -> Result { - build_add_plan_with_registered_providers( - query, - self.github_transport.as_ref(), - &self.providers, - policy, - ) - } - - pub fn build_add_plan_with_reporter( - &self, - query: &str, - reporter: &mut impl ProgressReporter, - policy: AddSecurityPolicy, - ) -> Result { - build_add_plan_with_reporter_and_registered_providers( - query, - self.github_transport.as_ref(), - reporter, - &self.providers, - policy, - ) - } - - pub fn install_app( - &self, - query: &str, - plan: &AddPlan, - install_home: &Path, - requested_scope: InstallScope, - reporter: &mut impl ProgressReporter, - ) -> Result { - install_app_with_reporter(query, plan, install_home, requested_scope, reporter) - } - - pub fn show( - &self, - query: &str, - installed_apps: &[AppRecord], - ) -> Result { - build_show_result_with(query, installed_apps, self.github_transport.as_ref()) - } - - pub fn show_all(&self, installed_apps: &[AppRecord]) -> Vec { - crate::app::show::build_installed_show_results(installed_apps) - } - - pub fn list(&self, apps: &[AppRecord]) -> Vec { - build_list_rows(apps) - } - - pub fn build_update_plan( - &self, - apps: &[AppRecord], - ) -> Result { - build_update_plan(apps) - } - - pub fn execute_updates( - &self, - apps: &[AppRecord], - install_home: &Path, - reporter: &mut impl ProgressReporter, - policy: AddSecurityPolicy, - ) -> Result { - execute_updates_with_reporter_and_policy(apps, install_home, reporter, policy) - } - - pub fn remove_registered_app( - &self, - query: &str, - apps: &[AppRecord], - install_home: &Path, - ) -> Result { - remove_registered_app(query, apps, install_home) - } -} - -impl<'a> UpmAppBuilder<'a> { - pub fn with_github_transport(mut self, github_transport: Box) -> Self { - self.github_transport = Some(github_transport); - self - } - - pub fn with_provider_registry(mut self, providers: ProviderRegistry<'a>) -> Self { - self.providers = providers; - self - } - - pub fn build(self) -> UpmApp<'a> { - UpmApp { - github_transport: self.github_transport.unwrap_or_else(default_transport), - providers: self.providers, - } - } -} - -fn default_provider_registry() -> ProviderRegistry<'static> { - ProviderRegistry::default() - .with_search_provider(upm_appimage::AppImageHubSearchProvider::new( - upm_appimage::source::appimagehub::default_transport(), - )) - .with_external_add_provider(upm_appimage::AppImageHubAddProvider::new( - upm_appimage::source::appimagehub::default_transport(), - )) -} diff --git a/crates/upm-core/src/app/providers.rs b/crates/upm-core/src/app/providers.rs deleted file mode 100644 index ba3307e..0000000 --- a/crates/upm-core/src/app/providers.rs +++ /dev/null @@ -1 +0,0 @@ -pub use upm_module_api::app::providers::*; diff --git a/crates/upm-core/src/domain/search.rs b/crates/upm-core/src/domain/search.rs deleted file mode 100644 index 437c024..0000000 --- a/crates/upm-core/src/domain/search.rs +++ /dev/null @@ -1 +0,0 @@ -pub use upm_module_api::domain::search::*; diff --git a/crates/upm-core/src/domain/source.rs b/crates/upm-core/src/domain/source.rs deleted file mode 100644 index 45d43ae..0000000 --- a/crates/upm-core/src/domain/source.rs +++ /dev/null @@ -1 +0,0 @@ -pub use upm_module_api::domain::source::*; diff --git a/crates/upm-core/tests/application_facade.rs b/crates/upm-core/tests/application_facade.rs deleted file mode 100644 index 4e84664..0000000 --- a/crates/upm-core/tests/application_facade.rs +++ /dev/null @@ -1,124 +0,0 @@ -use upm_core::adapters::traits::AdapterResolution; -use upm_core::app::UpmApp; -use upm_core::app::add::AddSecurityPolicy; -use upm_core::app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry}; -use upm_core::app::search::{SearchProvider, SearchProviderError}; -use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult}; -use upm_core::domain::source::{ - NormalizedSourceKind, ResolvedRelease, SourceInputKind, SourceKind, SourceRef, -}; -use upm_core::domain::update::{ - ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy, -}; -use upm_core::source::github::FixtureGitHubTransport; - -struct StubSearchProvider; - -impl SearchProvider for StubSearchProvider { - fn search(&self, _query: &SearchQuery) -> Result, SearchProviderError> { - Ok(vec![SearchResult { - provider_id: "external-search".to_owned(), - display_name: "Firefox Nightly".to_owned(), - description: Some("Provided by facade-owned providers".to_owned()), - source_locator: "https://example.invalid/firefox-nightly".to_owned(), - install_query: "external/firefox-nightly".to_owned(), - canonical_locator: "external/firefox-nightly".to_owned(), - version: Some("2026.03.21".to_owned()), - install_status: SearchInstallStatus::Available, - }]) - } -} - -struct StubExternalAddProvider; - -impl ExternalAddProvider for StubExternalAddProvider { - fn id(&self) -> &'static str { - "stub-appimage" - } - - fn resolve( - &self, - source: &SourceRef, - ) -> Result, upm_core::adapters::traits::AdapterError> { - Ok( - (source.kind == SourceKind::AppImageHub).then(|| ExternalAddResolution { - resolution: AdapterResolution { - source: SourceRef { - kind: SourceKind::AppImageHub, - locator: source.locator.clone(), - input_kind: SourceInputKind::AppImageHubShorthand, - normalized_kind: NormalizedSourceKind::AppImageHub, - canonical_locator: Some("2338455".to_owned()), - requested_tag: None, - requested_asset_name: None, - tracks_latest: true, - }, - release: ResolvedRelease { - version: "stable".to_owned(), - prerelease: false, - }, - }, - selected_artifact: ArtifactCandidate { - url: "https://downloads.example.invalid/firefox.AppImage".to_owned(), - version: "stable".to_owned(), - arch: Some("x86_64".to_owned()), - trusted_checksum: None, - weak_checksum_md5: Some("deadbeef".to_owned()), - selection_reason: "provider-release".to_owned(), - }, - update_strategy: UpdateStrategy { - preferred: ChannelPreference { - kind: UpdateChannelKind::DirectAsset, - locator: "https://downloads.example.invalid/firefox.AppImage".to_owned(), - reason: "provider-release".to_owned(), - }, - alternates: Vec::new(), - }, - display_name_hint: Some( - "Firefox by Mozilla - Official AppImage Edition".to_owned(), - ), - }), - ) - } -} - -#[test] -fn upm_app_can_be_constructed_without_cli_owned_module_composition() { - let _app = UpmApp::new(); -} - -#[test] -fn upm_app_search_delegates_through_the_application_facade() { - let app = UpmApp::builder() - .with_github_transport(Box::new(FixtureGitHubTransport)) - .with_provider_registry( - ProviderRegistry::default().with_search_provider(StubSearchProvider), - ) - .build(); - - let results = app.search(&SearchQuery::new("firefox"), &[]).unwrap(); - - assert!(results.remote_hits.iter().any(|hit| { - hit.provider_id == "external-search" && hit.install_query == "external/firefox-nightly" - })); -} - -#[test] -fn upm_app_add_planning_delegates_through_the_application_facade() { - let app = UpmApp::builder() - .with_github_transport(Box::new(FixtureGitHubTransport)) - .with_provider_registry( - ProviderRegistry::default().with_external_add_provider(StubExternalAddProvider), - ) - .build(); - - let plan = app - .build_add_plan("appimagehub/2338455", AddSecurityPolicy::default()) - .unwrap(); - - assert_eq!(plan.resolution.source.kind, SourceKind::AppImageHub); - assert_eq!( - plan.selected_artifact.url, - "https://downloads.example.invalid/firefox.AppImage" - ); -} diff --git a/crates/upm-core/tests/install_paths.rs b/crates/upm-core/tests/install_paths.rs deleted file mode 100644 index 72a503d..0000000 --- a/crates/upm-core/tests/install_paths.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::path::Path; - -use upm_core::domain::app::InstallScope; -use upm_core::integration::paths::{desktop_entry_path, managed_appimage_path}; - -#[test] -fn user_scope_path_lands_under_home_managed_dir() { - let path = managed_appimage_path(Path::new("/home/test"), InstallScope::User, "bat"); - - assert_eq!( - path, - Path::new("/home/test/.local/lib/upm/appimages/bat.AppImage") - ); -} - -#[test] -fn system_scope_path_lands_under_opt_upm_dir() { - let path = managed_appimage_path(Path::new("/home/test"), InstallScope::System, "bat"); - - assert_eq!(path, Path::new("/opt/upm/appimages/bat.AppImage")); -} - -#[test] -fn system_scope_desktop_entry_uses_upm_prefix() { - let path = desktop_entry_path(Path::new("/home/test"), InstallScope::System, "bat"); - - assert_eq!(path, Path::new("/usr/share/applications/upm-bat.desktop")); -} diff --git a/crates/upm-core/tests/provider_registry.rs b/crates/upm-core/tests/provider_registry.rs deleted file mode 100644 index 4d25130..0000000 --- a/crates/upm-core/tests/provider_registry.rs +++ /dev/null @@ -1,151 +0,0 @@ -use upm_core::app::add::{AddSecurityPolicy, build_add_plan_with_registered_providers}; -use upm_core::app::providers::{ExternalAddProvider, ExternalAddResolution, ProviderRegistry}; -use upm_core::app::search::{SearchProvider, build_search_results_with_registered_providers}; -use upm_core::domain::search::{SearchInstallStatus, SearchQuery, SearchResult}; -use upm_core::domain::source::{ - NormalizedSourceKind, ResolvedRelease, SourceInputKind, SourceKind, SourceRef, -}; -use upm_core::domain::update::{ - ArtifactCandidate, ChannelPreference, UpdateChannelKind, UpdateStrategy, -}; -use upm_core::source::github::FixtureGitHubTransport; - -struct StubSearchProvider; - -impl SearchProvider for StubSearchProvider { - fn search( - &self, - _query: &SearchQuery, - ) -> Result, upm_core::app::search::SearchProviderError> { - Ok(vec![SearchResult { - provider_id: "external-search".to_owned(), - display_name: "Firefox Nightly".to_owned(), - description: Some("Provided by external registry".to_owned()), - source_locator: "https://example.invalid/firefox-nightly".to_owned(), - install_query: "external/firefox-nightly".to_owned(), - canonical_locator: "external/firefox-nightly".to_owned(), - version: Some("2026.03.21".to_owned()), - install_status: SearchInstallStatus::Available, - }]) - } -} - -struct StubExternalAddProvider; - -impl ExternalAddProvider for StubExternalAddProvider { - fn id(&self) -> &'static str { - "stub-appimage" - } - - fn resolve( - &self, - source: &SourceRef, - ) -> Result, upm_core::adapters::traits::AdapterError> { - Ok( - (source.kind == SourceKind::AppImageHub).then(|| ExternalAddResolution { - resolution: upm_core::adapters::traits::AdapterResolution { - source: SourceRef { - kind: SourceKind::AppImageHub, - locator: source.locator.clone(), - input_kind: SourceInputKind::AppImageHubShorthand, - normalized_kind: NormalizedSourceKind::AppImageHub, - canonical_locator: Some("2338455".to_owned()), - requested_tag: None, - requested_asset_name: None, - tracks_latest: true, - }, - release: ResolvedRelease { - version: "stable".to_owned(), - prerelease: false, - }, - }, - selected_artifact: ArtifactCandidate { - url: "https://downloads.example.invalid/firefox.AppImage".to_owned(), - version: "stable".to_owned(), - arch: Some("x86_64".to_owned()), - trusted_checksum: None, - weak_checksum_md5: Some("deadbeef".to_owned()), - selection_reason: "provider-release".to_owned(), - }, - update_strategy: UpdateStrategy { - preferred: ChannelPreference { - kind: UpdateChannelKind::DirectAsset, - locator: "https://downloads.example.invalid/firefox.AppImage".to_owned(), - reason: "provider-release".to_owned(), - }, - alternates: Vec::new(), - }, - display_name_hint: Some( - "Firefox by Mozilla - Official AppImage Edition".to_owned(), - ), - }), - ) - } -} - -#[test] -fn build_search_results_with_registered_providers_uses_external_hits() { - let query = SearchQuery::new("firefox"); - let providers = ProviderRegistry::default().with_search_provider(StubSearchProvider); - - let results = build_search_results_with_registered_providers(&query, &[], &providers).unwrap(); - - let external_hit = results - .remote_hits - .iter() - .find(|hit| hit.provider_id == "external-search") - .unwrap(); - - assert_eq!(external_hit.install_query, "external/firefox-nightly"); - assert!( - results - .remote_hits - .iter() - .all(|hit| hit.provider_id != "appimagehub") - ); -} - -#[test] -fn build_add_plan_with_registered_providers_requires_external_provider_for_appimagehub() { - let registry = ProviderRegistry::default(); - - let error = build_add_plan_with_registered_providers( - "appimagehub/2338455", - &FixtureGitHubTransport, - ®istry, - AddSecurityPolicy::default(), - ) - .unwrap_err(); - - assert!(matches!( - error, - upm_core::app::add::BuildAddPlanError::NoInstallableArtifact { .. } - )); -} - -#[test] -fn build_add_plan_with_registered_providers_delegates_appimagehub_like_sources() { - let registry = ProviderRegistry::default().with_external_add_provider(StubExternalAddProvider); - - let plan = build_add_plan_with_registered_providers( - "appimagehub/2338455", - &FixtureGitHubTransport, - ®istry, - AddSecurityPolicy::default(), - ) - .unwrap(); - - assert_eq!(plan.resolution.source.kind, SourceKind::AppImageHub); - assert_eq!( - plan.resolution.source.canonical_locator.as_deref(), - Some("2338455") - ); - assert_eq!( - plan.selected_artifact.url, - "https://downloads.example.invalid/firefox.AppImage" - ); - assert_eq!( - plan.display_name_hint.as_deref(), - Some("Firefox by Mozilla - Official AppImage Edition") - ); -} diff --git a/crates/upm-module-api/Cargo.toml b/crates/upm-module-api/Cargo.toml deleted file mode 100644 index 93b9f31..0000000 --- a/crates/upm-module-api/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "upm-module-api" -version.workspace = true -edition.workspace = true -license.workspace = true - -[lib] -path = "src/lib.rs" - -[dependencies] -serde.workspace = true \ No newline at end of file diff --git a/crates/upm-module-api/src/adapters/mod.rs b/crates/upm-module-api/src/adapters/mod.rs deleted file mode 100644 index f6ac8fc..0000000 --- a/crates/upm-module-api/src/adapters/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod traits; diff --git a/crates/upm-module-api/src/app/mod.rs b/crates/upm-module-api/src/app/mod.rs deleted file mode 100644 index 532c582..0000000 --- a/crates/upm-module-api/src/app/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod providers; -pub mod search; diff --git a/crates/upm-module-api/src/app/providers.rs b/crates/upm-module-api/src/app/providers.rs deleted file mode 100644 index 3b24bb4..0000000 --- a/crates/upm-module-api/src/app/providers.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::adapters::traits::{AdapterError, AdapterResolution}; -use crate::app::search::SearchProvider; -use crate::domain::source::SourceRef; -use crate::domain::update::{ArtifactCandidate, UpdateStrategy}; - -pub trait ExternalAddProvider { - fn id(&self) -> &'static str; - - fn resolve(&self, source: &SourceRef) -> Result, AdapterError>; -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ExternalAddResolution { - pub resolution: AdapterResolution, - pub selected_artifact: ArtifactCandidate, - pub update_strategy: UpdateStrategy, - pub display_name_hint: Option, -} - -#[derive(Default)] -pub struct ProviderRegistry<'a> { - pub search_providers: Vec>, - pub external_add_providers: Vec>, -} - -impl<'a> ProviderRegistry<'a> { - pub fn with_search_provider

(mut self, provider: P) -> Self - where - P: SearchProvider + 'a, - { - self.search_providers.push(Box::new(provider)); - self - } - - pub fn with_external_add_provider

(mut self, provider: P) -> Self - where - P: ExternalAddProvider + 'a, - { - self.external_add_providers.push(Box::new(provider)); - self - } -} diff --git a/crates/upm-module-api/src/app/search.rs b/crates/upm-module-api/src/app/search.rs deleted file mode 100644 index c1caff4..0000000 --- a/crates/upm-module-api/src/app/search.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::domain::search::SearchResult; - -pub trait SearchProvider { - fn search( - &self, - query: &crate::domain::search::SearchQuery, - ) -> Result, 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(), - } - } -} diff --git a/crates/upm-module-api/src/domain/mod.rs b/crates/upm-module-api/src/domain/mod.rs deleted file mode 100644 index e397353..0000000 --- a/crates/upm-module-api/src/domain/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod search; -pub mod source; -pub mod update; diff --git a/crates/upm-module-api/src/domain/update.rs b/crates/upm-module-api/src/domain/update.rs deleted file mode 100644 index e7538a7..0000000 --- a/crates/upm-module-api/src/domain/update.rs +++ /dev/null @@ -1,42 +0,0 @@ -#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub enum UpdateChannelKind { - GitHubReleases, - ElectronBuilder, - Zsync, - DirectAsset, -} - -impl UpdateChannelKind { - pub fn as_str(&self) -> &'static str { - match self { - Self::GitHubReleases => "github-releases", - Self::ElectronBuilder => "electron-builder", - Self::Zsync => "zsync", - Self::DirectAsset => "direct-asset-lineage", - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct ChannelPreference { - pub kind: UpdateChannelKind, - pub locator: String, - pub reason: String, -} - -#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct UpdateStrategy { - pub preferred: ChannelPreference, - #[serde(default)] - pub alternates: Vec, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ArtifactCandidate { - pub url: String, - pub version: String, - pub arch: Option, - pub trusted_checksum: Option, - pub weak_checksum_md5: Option, - pub selection_reason: String, -} diff --git a/crates/upm-module-api/src/lib.rs b/crates/upm-module-api/src/lib.rs deleted file mode 100644 index 80690c1..0000000 --- a/crates/upm-module-api/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod adapters; -pub mod app; -pub mod domain; diff --git a/crates/upm/src/providers.rs b/crates/upm/src/providers.rs deleted file mode 100644 index f14e5c3..0000000 --- a/crates/upm/src/providers.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub fn application() -> upm_core::UpmApp<'static> { - upm_core::UpmApp::new() -} diff --git a/crates/upm/tests/config_loading.rs b/crates/upm/tests/config_loading.rs deleted file mode 100644 index badd37a..0000000 --- a/crates/upm/tests/config_loading.rs +++ /dev/null @@ -1,167 +0,0 @@ -use std::sync::Mutex; - -use tempfile::tempdir; -use upm::config::{ - CliConfig, ConfigError, SearchConfig, ThemeConfig, default_path, load_from_path, -}; -use upm::default_registry_path; - -static ENV_LOCK: Mutex<()> = Mutex::new(()); - -struct EnvGuard { - key: &'static str, - original: Option, -} - -impl EnvGuard { - fn set(key: &'static str, value: impl AsRef) -> Self { - let original = std::env::var_os(key); - unsafe { - std::env::set_var(key, value); - } - Self { key, original } - } - - fn remove(key: &'static str) -> Self { - let original = std::env::var_os(key); - unsafe { - std::env::remove_var(key); - } - Self { key, original } - } -} - -impl Drop for EnvGuard { - fn drop(&mut self) { - match &self.original { - Some(value) => unsafe { - std::env::set_var(self.key, value); - }, - None => unsafe { - std::env::remove_var(self.key); - }, - } - } -} - -#[test] -fn missing_config_file_returns_defaults() { - let dir = tempdir().unwrap(); - let path = dir.path().join("config.toml"); - - let config = load_from_path(&path).unwrap(); - - assert_eq!(config, CliConfig::default()); - assert_eq!(config.search, SearchConfig::default()); - assert!(!config.allow_http); - assert!(config.search.bottom_to_top); - assert!(!config.search.skip_confirmation); - assert_eq!(config.theme.accent, "#b388ff"); - assert_eq!(config.theme.accent_secondary, "#d5c2ff"); - assert_eq!(config.theme.dim, "#7f7396"); -} - -#[test] -fn search_section_overrides_defaults() { - let dir = tempdir().unwrap(); - let path = dir.path().join("config.toml"); - std::fs::write( - &path, - "allow_http = true\n\n[search]\nbottom_to_top = false\nskip_confirmation = true\n\n[theme]\naccent = \"#9f6bff\"\naccent_secondary = \"#efe7ff\"\ndim = \"#6b6480\"\n", - ) - .unwrap(); - - let config = load_from_path(&path).unwrap(); - - assert_eq!( - config, - CliConfig { - allow_http: true, - search: SearchConfig { - bottom_to_top: false, - skip_confirmation: true, - }, - theme: ThemeConfig { - accent: "#9f6bff".to_owned(), - accent_secondary: "#efe7ff".to_owned(), - dim: "#6b6480".to_owned(), - }, - } - ); -} - -#[test] -fn malformed_toml_returns_path_aware_error() { - let dir = tempdir().unwrap(); - let path = dir.path().join("config.toml"); - std::fs::write(&path, "[search\nskip_confirmation = true\n").unwrap(); - - let error = load_from_path(&path).unwrap_err(); - - match error { - ConfigError::Parse { - path: error_path, .. - } => { - assert_eq!(error_path, path); - } - other => panic!("expected parse error, got {other:?}"), - } -} - -#[test] -fn default_config_path_uses_upm_directory() { - let _guard = ENV_LOCK.lock().unwrap(); - let dir = tempdir().unwrap(); - - let _config_path = EnvGuard::remove("UPM_CONFIG_PATH"); - let _xdg_config_home = EnvGuard::remove("XDG_CONFIG_HOME"); - let _home = EnvGuard::set("HOME", dir.path()); - - let path = default_path(); - - assert_eq!(path, dir.path().join(".config/upm/config.toml")); -} - -#[test] -fn default_config_path_ignores_legacy_aim_override() { - let _guard = ENV_LOCK.lock().unwrap(); - let dir = tempdir().unwrap(); - let legacy_path = dir.path().join("aim-config.toml"); - - let _legacy_config_path = EnvGuard::set("AIM_CONFIG_PATH", &legacy_path); - let _config_path = EnvGuard::remove("UPM_CONFIG_PATH"); - let _xdg_config_home = EnvGuard::remove("XDG_CONFIG_HOME"); - let _home = EnvGuard::set("HOME", dir.path()); - - let path = default_path(); - - assert_eq!(path, dir.path().join(".config/upm/config.toml")); -} - -#[test] -fn default_registry_path_uses_upm_directory() { - let _guard = ENV_LOCK.lock().unwrap(); - let dir = tempdir().unwrap(); - - let _registry_path = EnvGuard::remove("UPM_REGISTRY_PATH"); - let _home = EnvGuard::set("HOME", dir.path()); - - let path = default_registry_path(); - - assert_eq!(path, dir.path().join(".local/share/upm/registry.toml")); -} - -#[test] -fn default_registry_path_ignores_legacy_aim_override() { - let _guard = ENV_LOCK.lock().unwrap(); - let dir = tempdir().unwrap(); - let legacy_path = dir.path().join("aim-registry.toml"); - - let _legacy_registry_path = EnvGuard::set("AIM_REGISTRY_PATH", &legacy_path); - let _registry_path = EnvGuard::remove("UPM_REGISTRY_PATH"); - let _home = EnvGuard::set("HOME", dir.path()); - - let path = default_registry_path(); - - assert_eq!(path, dir.path().join(".local/share/upm/registry.toml")); -}