From df342730ab1581648b8546dada9b5cb3fb72ee6a Mon Sep 17 00:00:00 2001 From: stoorps Date: Thu, 19 Mar 2026 21:10:33 +0000 Subject: [PATCH] feat: enhance .gitignore and add per-distro installation design and implementation plans --- .gitignore | 1 + ...26-03-19-per-distro-installation-design.md | 276 ++++++++++++++ ...distro-installation-implementation-plan.md | 341 ++++++++++++++++++ 3 files changed, 618 insertions(+) create mode 100644 .plans/002-per-distro-installation/2026-03-19-per-distro-installation-design.md create mode 100644 .plans/002-per-distro-installation/2026-03-19-per-distro-installation-implementation-plan.md diff --git a/.gitignore b/.gitignore index ea8c4bf..a6adf0d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.worktrees diff --git a/.plans/002-per-distro-installation/2026-03-19-per-distro-installation-design.md b/.plans/002-per-distro-installation/2026-03-19-per-distro-installation-design.md new file mode 100644 index 0000000..b979ec5 --- /dev/null +++ b/.plans/002-per-distro-installation/2026-03-19-per-distro-installation-design.md @@ -0,0 +1,276 @@ +# Per-Distro Installation Design + +## Goal + +Implement actual AppImage installation for `aim` across a broad Linux distro set, while preserving a single reusable install engine in `aim-core`, keeping `aim-cli` thin, and respecting distro policy constraints instead of fighting them. + +## Agreed Product Shape + +### Supported distro families + +- Debian +- Fedora +- Arch +- openSUSE +- Alpine +- Nix +- Immutable +- Generic Linux fallback + +### Detection model + +- Detect distro family from `/etc/os-release` +- Detect policy constraints such as immutable or Nix-style environments +- Probe runtime capabilities such as directory writability and optional helper availability +- Let distro family define intended policy, then let capabilities refine the safe final action + +### Support posture + +- Broad Linux coverage is the target +- Deep integration is preferred for mainstream distro families +- Immutable systems should get best-effort user installs with explicit warnings +- System installs should prefer native shared integration locations when policy allows, with fallback to `aim`-managed locations where appropriate + +## Recommended Approach + +Use a hybrid family-plus-capability model. + +This was chosen over a family-only matrix because distro identity alone is not enough to determine what the current host will allow. It was also chosen over capability-only probing because distro family still matters for native path expectations and default policy. + +The model is: + +- distro family selects the intended install policy +- host capabilities determine whether that policy can be executed fully, degraded, or not at all +- one generic install engine executes the workflow using the resolved policy + +## Install Policy Model + +### Debian, Fedora, Arch, openSUSE + +- `--user` installs use `aim`-managed user paths +- `--system` prefers native shared desktop integration locations and system icon locations +- AppImage payloads remain in an `aim`-managed system payload directory unless a later distro-specific need proves otherwise +- Desktop caches are refreshed only when helper tools are present + +### Alpine + +- Treat Alpine as a lightweight Linux with shallower integration assumptions +- Prefer `aim`-managed paths for both user and system installs +- Run optional helpers only when present + +### Nix + +- Default to conservative behavior +- Allow `--user` installs into `aim`-managed user paths, with a warning that the install is outside declarative package management +- Deny `--system` installs until a Nix-native strategy exists +- Never attempt to write into the Nix store or emulate a declarative install + +### Immutable + +- Treat `--user` as the primary supported path +- If `--system` is requested, first test whether the host genuinely permits the required writes +- If policy blocks system writes, either downgrade to user scope with an explicit warning or fail clearly when downgrade is not allowed by policy + +### Generic fallback + +- Use `aim`-managed paths only +- Apply standards-based XDG desktop integration only +- Skip distro-specific helper assumptions + +### Cross-cutting rule + +Payload storage and integration targets are separate concerns. + +For example, a system install may place the AppImage payload under `/opt/aim/appimages`, while writing the `.desktop` file to `/usr/share/applications` and icons to `/usr/share/icons`. This keeps artifact replacement and rollback simple while still integrating natively. + +## Execution Pipeline + +Use one install engine with policy injected into it rather than separate installers per distro. + +### 1. Resolve policy + +- Detect distro family +- Detect capabilities and policy markers +- Resolve an `InstallPolicy` describing: + - allowed scope + - payload root + - desktop entry root + - icon root + - integration mode + - helper refresh actions + - warnings and fallback notes + +### 2. Stage artifact + +- Download into a temporary staging path +- Validate that the payload is an AppImage +- Derive stable identity and final filenames before touching permanent locations +- Mark the staged artifact executable before final commit + +### 3. Commit payload atomically + +- Move the staged AppImage into the final managed payload location +- Use replacement semantics suitable for safe updates +- Keep the old installed payload until the new one is validated and committed + +### 4. Commit integration + +- Generate a normalized `.desktop` file +- Extract and install icons when available +- Write integration artifacts into policy-selected locations +- Keep these artifacts generated by `aim`, not manually managed + +### 5. Refresh caches best-effort + +- Run optional helpers only when present and relevant +- Cache refresh failures should produce warnings, not rollback a valid install + +### 6. Persist registry last + +- Only persist the final `AppRecord` after payload and required integration steps succeed +- The registry should represent completed installs, not partial attempts + +## Reliability Model + +### Preflight failures + +Stop early for: + +- unsupported scope for the resolved host policy +- unwritable required target directories with no allowed fallback +- payloads that are not valid AppImages +- missing required normalized metadata for integration + +### Transaction boundary + +Treat install as three phases: + +- preflight and staging +- payload commit +- integration commit + +Nothing is persisted in the registry until the required phases succeed. + +### Rollback rules + +- If staging fails, clean temporary files only +- If payload commit succeeds but required integration fails, remove the new payload unless degraded payload-only success was explicitly allowed +- If integration partially succeeds, remove generated `.desktop` and icon artifacts before returning failure +- Cache refresh failures do not trigger rollback + +### Degraded success + +Success with warnings is acceptable only when: + +- optional cache refresh helpers are missing or fail +- desktop integration was optional and the host lacks the helper stack to finish niceties +- immutable or policy-heavy systems forced a user-scope fallback that policy explicitly allows + +### Update safety + +- Updates stage side-by-side and replace atomically +- The previous working AppImage stays active until the new one is validated and committed +- Failed updates should leave the previous installed version intact + +## Verification Strategy + +### Unit tests + +- distro detection +- immutable and Nix marker detection +- helper capability probing +- writable path probing +- install policy resolution per family and scope +- degraded and denied policy decisions + +### Integration tests + +- successful user install +- successful system install using managed payload plus native integration paths +- denied Nix system install +- immutable system fallback to user install with warning +- payload commit failure cleanup +- integration failure rollback +- cache refresh warning-only behavior + +Fixture-driven filesystem tests should cover most of this. First implementation should not depend on real distro images in CI. + +## Architecture Slice + +### Core types + +#### `DistroFamily` + +- `Debian` +- `Fedora` +- `Arch` +- `OpenSuse` +- `Alpine` +- `Nix` +- `Immutable` +- `Generic` + +#### `HostCapabilities` + +- parsed `os-release` identity +- immutable and Nix policy markers +- writable status for candidate directories +- desktop-session presence +- helper availability such as `update-desktop-database` and `gtk-update-icon-cache` + +#### `InstallPolicy` + +- resolved scope +- payload path root +- desktop entry root +- icon path root +- integration mode: `full`, `degraded`, `payload-only`, or `denied` +- fallback notes and warnings for UI surfaces + +#### `InstallPlan` + +- selected artifact URL and expected identity +- staging paths +- final payload path +- desktop file path +- icon path +- helper refresh actions to attempt after commit + +#### `InstallOutcome` + +- installed scope +- resolved mode +- written paths +- warnings +- rollback status + +### Recommended module layout + +Within `crates/aim-core/src/`: + +- `platform/distro.rs` +- `platform/capabilities.rs` +- `integration/policy.rs` +- `integration/install.rs` +- `integration/desktop.rs` +- `integration/refresh.rs` + +Responsibilities: + +- `platform/distro.rs` detects distro families and policy markers +- `platform/capabilities.rs` probes writability and helper availability +- `integration/policy.rs` converts requested scope plus host facts into `InstallPolicy` +- `integration/install.rs` owns the transactional install pipeline +- `integration/desktop.rs` owns desktop file generation and icon handling +- `integration/refresh.rs` owns best-effort helper execution + +### CLI behavior + +- `aim ` should become a real install path rather than registry-only tracking +- The CLI should print the resolved install mode before commit +- If the plan degrades, the CLI should explain that before confirmation +- Bare `aim` can later reuse the same policy summary when showing pending updates + +## Summary + +The agreed design is a single install engine in `aim-core` driven by a resolved per-host `InstallPolicy`. Distro families provide intended policy, runtime capabilities decide what is safe, and transactional install semantics keep installs recoverable. This preserves the thin-CLI architecture while allowing broad Linux coverage without scattering distro-specific conditionals across the entire codebase. \ No newline at end of file diff --git a/.plans/002-per-distro-installation/2026-03-19-per-distro-installation-implementation-plan.md b/.plans/002-per-distro-installation/2026-03-19-per-distro-installation-implementation-plan.md new file mode 100644 index 0000000..a6cfb9b --- /dev/null +++ b/.plans/002-per-distro-installation/2026-03-19-per-distro-installation-implementation-plan.md @@ -0,0 +1,341 @@ +# Per-Distro Installation Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Implement real AppImage installation in `aim-core` with distro-aware policy resolution, transactional payload and desktop integration, and CLI surfacing of resolved install mode and warnings. + +**Architecture:** Add a host detection and policy layer ahead of the existing install scaffolding, then turn `integration/install.rs` into a transactional executor that stages payloads, commits managed artifacts atomically, writes desktop integration into policy-selected locations, and only persists registry state after success. Keep distro-specific behavior declarative through `DistroFamily`, `HostCapabilities`, and `InstallPolicy` instead of branching throughout the pipeline. + +**Tech Stack:** Rust, Cargo workspace, std filesystem APIs, existing `aim-core` domain and registry types, `clap`, `dialoguer`, fixture-backed tests in `crates/aim-core/tests` and `crates/aim-cli/tests`. + +--- + +### Task 1: Add distro family detection and host capability probing + +**Files:** +- Create: `crates/aim-core/src/platform/distro.rs` +- Create: `crates/aim-core/src/platform/capabilities.rs` +- Modify: `crates/aim-core/src/platform/mod.rs` +- Test: `crates/aim-core/tests/platform_detection.rs` + +**Step 1: Write the failing test** + +```rust +use aim_core::platform::distro::{detect_distro_family, DistroFamily}; + +#[test] +fn detects_fedora_family_from_os_release() { + let distro = detect_distro_family("ID=fedora\nID_LIKE=rhel centos\n"); + assert_eq!(distro, DistroFamily::Fedora); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test detects_fedora_family_from_os_release --package aim-core --test platform_detection` +Expected: FAIL because distro detection types do not exist yet + +**Step 3: Write minimal implementation** + +Add: +- `DistroFamily` +- `/etc/os-release` parsing helpers +- immutable and Nix policy markers +- helper availability probing for desktop refresh commands +- directory writability probing for candidate install roots + +Keep the probing interfaces small and deterministic so tests can inject fake host facts. + +**Step 4: Run test to verify it passes** + +Run: `cargo test detects_fedora_family_from_os_release --package aim-core --test platform_detection` +Expected: PASS + +**Step 5: Commit** + +```bash +git add crates/aim-core/src/platform/distro.rs crates/aim-core/src/platform/capabilities.rs crates/aim-core/src/platform/mod.rs crates/aim-core/tests/platform_detection.rs +git commit -m "feat: add distro and capability detection" +``` + +### Task 2: Introduce install policy resolution + +**Files:** +- Create: `crates/aim-core/src/integration/policy.rs` +- Modify: `crates/aim-core/src/integration/mod.rs` +- Modify: `crates/aim-core/src/platform/mod.rs` +- Test: `crates/aim-core/tests/install_policy.rs` + +**Step 1: Write the failing test** + +```rust +use aim_core::integration::policy::{resolve_install_policy, IntegrationMode}; +use aim_core::platform::{DistroFamily, HostCapabilities, InstallScope}; + +#[test] +fn immutable_system_request_downgrades_to_user_when_allowed() { + let capabilities = HostCapabilities::immutable_user_only(); + let policy = resolve_install_policy(DistroFamily::Immutable, InstallScope::System, &capabilities).unwrap(); + + assert_eq!(policy.scope, InstallScope::User); + assert_eq!(policy.integration_mode, IntegrationMode::Degraded); + assert!(!policy.warnings.is_empty()); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test immutable_system_request_downgrades_to_user_when_allowed --package aim-core --test install_policy` +Expected: FAIL because install policy resolution does not exist yet + +**Step 3: Write minimal implementation** + +Create: +- `InstallPolicy` +- `IntegrationMode` +- policy resolution for the agreed distro families +- separation of payload, desktop, and icon roots +- warning collection for downgraded or conservative behavior + +Implement only the current agreed rules. Do not add speculative distro exceptions. + +**Step 4: Run test to verify it passes** + +Run: `cargo test immutable_system_request_downgrades_to_user_when_allowed --package aim-core --test install_policy` +Expected: PASS + +**Step 5: Commit** + +```bash +git add crates/aim-core/src/integration/policy.rs crates/aim-core/src/integration/mod.rs crates/aim-core/src/platform/mod.rs crates/aim-core/tests/install_policy.rs +git commit -m "feat: resolve per-distro install policy" +``` + +### Task 3: Turn install scaffolding into a staged payload executor + +**Files:** +- Modify: `crates/aim-core/src/integration/install.rs` +- Modify: `crates/aim-core/src/integration/paths.rs` +- Modify: `crates/aim-core/src/app/add.rs` +- Test: `crates/aim-core/tests/install_payload.rs` + +**Step 1: Write the failing test** + +```rust +use aim_core::integration::install::stage_and_commit_payload; + +#[test] +fn payload_commit_moves_staged_appimage_into_final_location() { + let outcome = stage_and_commit_payload(/* fixture inputs */).unwrap(); + assert!(outcome.final_payload_path.ends_with(".AppImage")); + assert!(outcome.final_payload_path.exists()); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test payload_commit_moves_staged_appimage_into_final_location --package aim-core --test install_payload` +Expected: FAIL because install execution still only exposes path helpers + +**Step 3: Write minimal implementation** + +Implement: +- staging download target creation +- AppImage validation hook +- executable bit application in staging +- atomic payload replacement into the managed payload root +- minimal rollback for payload commit failure + +Do not write registry state yet. + +**Step 4: Run test to verify it passes** + +Run: `cargo test payload_commit_moves_staged_appimage_into_final_location --package aim-core --test install_payload` +Expected: PASS + +**Step 5: Commit** + +```bash +git add crates/aim-core/src/integration/install.rs crates/aim-core/src/integration/paths.rs crates/aim-core/src/app/add.rs crates/aim-core/tests/install_payload.rs +git commit -m "feat: add staged payload install executor" +``` + +### Task 4: Add desktop integration and refresh handling + +**Files:** +- Create: `crates/aim-core/src/integration/desktop.rs` +- Create: `crates/aim-core/src/integration/refresh.rs` +- Modify: `crates/aim-core/src/integration/install.rs` +- Modify: `crates/aim-core/src/integration/mod.rs` +- Test: `crates/aim-core/tests/install_integration.rs` + +**Step 1: Write the failing test** + +```rust +use aim_core::integration::install::execute_install; + +#[test] +fn install_writes_desktop_entry_and_reports_refresh_warning_only() { + let outcome = execute_install(/* fixture with missing helper */).unwrap(); + + assert!(outcome.desktop_entry_path.unwrap().exists()); + assert!(!outcome.warnings.is_empty()); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test install_writes_desktop_entry_and_reports_refresh_warning_only --package aim-core --test install_integration` +Expected: FAIL because desktop integration and refresh steps do not exist yet + +**Step 3: Write minimal implementation** + +Add: +- `.desktop` generation from normalized metadata +- icon extraction and placement hooks +- refresh action planning +- best-effort helper execution for desktop database and icon cache refresh +- rollback of generated integration files when required integration fails + +Keep helper execution optional and warning-driven. + +**Step 4: Run test to verify it passes** + +Run: `cargo test install_writes_desktop_entry_and_reports_refresh_warning_only --package aim-core --test install_integration` +Expected: PASS + +**Step 5: Commit** + +```bash +git add crates/aim-core/src/integration/desktop.rs crates/aim-core/src/integration/refresh.rs crates/aim-core/src/integration/install.rs crates/aim-core/src/integration/mod.rs crates/aim-core/tests/install_integration.rs +git commit -m "feat: add desktop integration and refresh handling" +``` + +### Task 5: Persist registry state only after successful install and surface policy in the CLI + +**Files:** +- Modify: `crates/aim-core/src/app/add.rs` +- Modify: `crates/aim-core/src/registry/mod.rs` +- Modify: `crates/aim-cli/src/lib.rs` +- Modify: `crates/aim-cli/src/ui/prompt.rs` +- Modify: `crates/aim-cli/src/ui/render.rs` +- Test: `crates/aim-cli/tests/end_to_end_cli.rs` + +**Step 1: Write the failing test** + +```rust +#[test] +fn cli_add_installs_and_renders_resolved_mode() { + let output = run_cli_add(/* fixture query */); + + assert!(output.contains("installing as user")); + assert!(output.contains("installed app:")); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test cli_add_installs_and_renders_resolved_mode --package aim-cli --test end_to_end_cli` +Expected: FAIL because the CLI still performs registry-backed tracking only + +**Step 3: Write minimal implementation** + +Change the add flow so it: +- builds an install plan instead of only a tracking record +- prints the resolved install mode and warnings before commit +- persists the final `AppRecord` only after successful install completion +- renders installed outcomes rather than tracked-only outcomes + +Preserve review prompts where source ambiguity still exists. + +**Step 4: Run test to verify it passes** + +Run: `cargo test cli_add_installs_and_renders_resolved_mode --package aim-cli --test end_to_end_cli` +Expected: PASS + +**Step 5: Commit** + +```bash +git add crates/aim-core/src/app/add.rs crates/aim-core/src/registry/mod.rs crates/aim-cli/src/lib.rs crates/aim-cli/src/ui/prompt.rs crates/aim-cli/src/ui/render.rs crates/aim-cli/tests/end_to_end_cli.rs +git commit -m "feat: execute installs from cli add flow" +``` + +### Task 6: Lock down rollback and failure semantics + +**Files:** +- Modify: `crates/aim-core/src/integration/install.rs` +- Modify: `crates/aim-core/src/integration/desktop.rs` +- Test: `crates/aim-core/tests/install_failures.rs` +- Modify: `README.md` + +**Step 1: Write the failing test** + +```rust +use aim_core::integration::install::execute_install; + +#[test] +fn integration_failure_removes_new_payload_and_generated_files() { + let error = execute_install(/* fixture with forced desktop write failure */).unwrap_err(); + + assert!(error.to_string().contains("desktop integration failed")); + assert_install_root_is_clean(); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test integration_failure_removes_new_payload_and_generated_files --package aim-core --test install_failures` +Expected: FAIL because rollback behavior is not complete yet + +**Step 3: Write minimal implementation** + +Finish: +- rollback of newly committed payloads on required integration failure +- cleanup of generated desktop and icon artifacts +- warning-only handling for refresh failures +- README updates describing actual install behavior and degraded cases + +Do not broaden feature scope beyond the approved design. + +**Step 4: Run test to verify it passes** + +Run: `cargo test integration_failure_removes_new_payload_and_generated_files --package aim-core --test install_failures` +Expected: PASS + +**Step 5: Commit** + +```bash +git add crates/aim-core/src/integration/install.rs crates/aim-core/src/integration/desktop.rs crates/aim-core/tests/install_failures.rs README.md +git commit -m "feat: finalize install rollback behavior" +``` + +### Task 7: Run full workspace verification + +**Files:** +- Modify: none unless verification exposes regressions tied to the approved scope + +**Step 1: Run formatter** + +Run: `cargo fmt --check` +Expected: PASS + +**Step 2: Run lints** + +Run: `cargo clippy --workspace --all-targets --all-features -- -D warnings` +Expected: PASS + +**Step 3: Run full test suite** + +Run: `cargo test --workspace` +Expected: PASS + +**Step 4: Fix only scoped regressions if any appear** + +If verification fails, make the smallest design-consistent change needed and rerun the affected command before rerunning the full suite. + +**Step 5: Commit** + +```bash +git add -A +git commit -m "chore: verify per-distro installation implementation" +``` \ No newline at end of file