feat: enhance .gitignore and add per-distro installation design and implementation plans

This commit is contained in:
stoorps 2026-03-19 21:10:33 +00:00
parent caf870d05e
commit df342730ab
Signed by: stoorps
SSH key fingerprint: SHA256:AZlPfu9hTu042EGtZElmDQoy+KvMOeShLDan/fYLoNI
3 changed files with 618 additions and 0 deletions

View file

@ -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 <QUERY>` 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.

View file

@ -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"
```