diff --git a/designs/grel-rs/TECHNICAL_DESIGN.md b/designs/grel-rs/TECHNICAL_DESIGN.md index ccaef03..7d698e0 100644 --- a/designs/grel-rs/TECHNICAL_DESIGN.md +++ b/designs/grel-rs/TECHNICAL_DESIGN.md @@ -1,21 +1,22 @@ -# ๐Ÿ“˜ `grel-rs` Technical Design Document +# ๐Ÿ“˜ `grel-rs` Technical Design Document (v3.0) **Binary:** `grel` | **Repository:** `grel-rs` **Target Platforms:** Linux, Windows (macOS optional) -**Core Philosophy:** Forge-agnostic, pacman-familiar, performance-first, secure, extensible via traits. +**Core Philosophy:** Pure CLI, deterministic asset resolution, pacman/Artix-familiar UX, explicit over implicit, robust upgrade edge-case handling. --- ## 1. Overview & Goals -`grel` (Global/General Release) is a terminal-native, high-performance package manager for downloading and managing binary releases from Git forges. It abstracts provider-specific APIs into a unified resolution/download pipeline, supports HTTP/SOCKS5 proxies, caches optimal DNS endpoints, downloads assets in parallel, and presents a polished, pacman-compatible UX. +`grel` (Global/General Release) is a terminal-native, high-performance package manager for downloading and managing binary releases from Git forges. It abstracts provider-specific APIs into a unified resolution pipeline, supports HTTP/SOCKS5 proxies, caches optimal DNS endpoints, downloads assets in parallel, and delivers a transparent, scriptable, pacman-compatible UX. **Key Requirements Met:** -- โœ… Pacman-style CLI (`-S`, `-Syu`, `-Ss`, `--noconfirm`) -- โœ… Proxy support (HTTP/HTTPS/SOCKS5) with env/config fallback -- โœ… DNS/IP latency caching + RTT probing for CDN optimization -- โœ… Parallel asset downloads with multi-progress UI -- โœ… Interactive asset selection (pacman/artix style) with TTY/CI fallback -- โœ… Cross-platform (Linux/Windows) with safe extraction & PATH integration -- โœ… Extensible provider architecture (GitHub, GitLab, Codeberg, Gitea, self-hosted) +- โœ… Pure CLI (no TUI/GUI), standard `std::io` prompts +- โœ… Deterministic asset resolution: **filter โ†’ sort โ†’ explicit prompt** (zero scoring magic) +- โœ… Robust parsing of non-standard release names via `Os`/`Arch` enums + alias mapping +- โœ… Format control: `.deb`/`.rpm`/`.msi`/`.dmg` disabled by default +- โœ… Pacman/Artix-style numbered selection with `[default]`, TTY/CI fallback, `-y`/`--noconfirm` +- โœ… `-Syu` resilience: detects filename renames, handles repo 404s via orphan tracking, explicit migration path +- โœ… Cross-platform PATH integration (`grel path add`), XDG-compliant, no `sudo` +- โœ… Forge-agnostic provider routing (public + self-hosted URLs) --- @@ -24,72 +25,31 @@ grel-rs/ โ”œโ”€โ”€ Cargo.toml # Workspace root (resolver = "2") โ”œโ”€โ”€ crates/ -โ”‚ โ”œโ”€โ”€ grel-cli/ # CLI parsing, TUI orchestration, prompts, progress -โ”‚ โ”œโ”€โ”€ grel-core/ # Resolution, versioning, asset matching, upgrade state +โ”‚ โ”œโ”€โ”€ grel-cli/ # CLI parsing, pure-text prompts, progress routing +โ”‚ โ”œโ”€โ”€ grel-core/ # Resolution, tokenization, filter/sort pipeline, upgrade state โ”‚ โ”œโ”€โ”€ grel-providers/ # Forge trait, registry, API implementations (modules) -โ”‚ โ”œโ”€โ”€ grel-network/ # HTTP client, proxy routing, IP cache resolver, downloader +โ”‚ โ”œโ”€โ”€ grel-network/ # HTTP client, proxy routing, IP cache resolver, parallel downloader โ”‚ โ”œโ”€โ”€ grel-cache/ # SQLite state, IP cache, artifact storage, TTL eviction -โ”‚ โ””โ”€โ”€ grel-config/ # Layered config (CLI > env > TOML > defaults), migrations +โ”‚ โ””โ”€โ”€ grel-config/ # Layered config, migrations, asset priority matrices โ”œโ”€โ”€ tests/ # Integration, mock servers, e2e fixtures โ””โ”€โ”€ scripts/ # CI, release, benchmark helpers ``` -**Crate Boundaries:** -| Crate | Owns | Depends On | -|-------|------|------------| -| `grel-cli` | CLI, UI, command routing, prompts | `grel-core`, `grel-config`, `dialoguer`, `indicatif` | -| `grel-core` | Package resolution, asset matching, upgrade logic | `grel-providers`, `grel-cache`, `semver` | -| `grel-providers` | API clients, pagination, rate-limit handling | `reqwest`, `async-trait`, `chrono` | -| `grel-network` | Proxy, IP resolver, parallel downloads, streaming | `reqwest`, `hickory-resolver`, `dashmap`, `tokio` | -| `grel-cache` | SQLite state, IP cache, artifact storage, LRU | `sqlx`, `sha2`, `tar`/`zip`, `directories` | -| `grel-config` | Settings loading, validation, defaults, migrations | `figment`, `serde`, `toml` | +| Crate | Responsibility | +|-------|----------------| +| `grel-cli` | Subcommand routing, pure CLI prompt loops, `indicatif` progress, `tabled` output | +| `grel-core` | `PackageRef` parsing, `AssetTokens` extraction, deterministic filter/sort, upgrade planning | +| `grel-providers` | `ReleaseProvider` trait, GitHub/GitLab/Gitea/Codeberg modules, self-hosted auto-detection | +| `grel-network` | Proxy chaining, DNS/IP cache resolver, `JoinSet` parallel downloads, streaming extraction | +| `grel-cache` | `state.sqlite` management, ETag/IP cache, LRU artifact eviction, atomic install temp dirs | +| `grel-config` | TOML loading, env/CLI overrides, schema validation, config migrations | --- -## 3. Core Abstractions & Data Flow -All forges implement a single normalized contract. `grel-core` parses user input โ†’ providers return forge-agnostic structs โ†’ `grel-network` downloads blindly. - -```rust -pub enum ForgeType { GitHub, GitLab, Gitea, Codeberg, SelfHosted(String) } - -pub struct PackageRef { - pub forge: ForgeType, - pub owner: String, pub repo: String, - pub version: Option, - pub prerelease: bool, -} - -pub struct ResolvedRelease { - pub version: semver::Version, - pub published_at: chrono::DateTime, - pub is_prerelease: bool, - pub assets: Vec, - pub release_notes: Option, -} - -pub struct DownloadAsset { - pub filename: String, - pub download_url: url::Url, - pub size_bytes: u64, - pub sha256: Option, - pub asset_type: AssetKind, // Binary, Archive, Source, Other -} -``` - -**Flow:** -``` -CLI โ†’ PackageRef โ†’ ProviderRegistry โ†’ Provider.resolve() โ†’ ResolvedRelease - โ†’ AssetResolver (tiered match + checksum pairing) โ†’ SelectedRelease - โ†’ Network (IP cache โ†’ proxy client โ†’ parallel download) - โ†’ Cache (verify sha256 โ†’ extract โ†’ atomic rename โ†’ update state.sqlite) - โ†’ CLI (progress summary โ†’ exit) -``` - ---- - -## 4. CLI Specification & UX Design -`clap 4` with subcommands. Pacman short flags supported via aliases. +## 3. CLI Specification & Pure UX +No TUI frameworks. All interaction uses standard terminal I/O with predictable, pipe-safe behavior. +### Commands & Aliases | Command | Aliases | Description | |---------|---------|-------------| | `grel sync foo/bar` | `S`, `-S` | Download & install latest | @@ -97,51 +57,82 @@ CLI โ†’ PackageRef โ†’ ProviderRegistry โ†’ Provider.resolve() โ†’ ResolvedRelea | `grel search keyword` | `Ss`, `-Ss` | Search across registered forges | | `grel upgrade` | `Syu`, `-Syu` | Refresh + upgrade installed | | `grel info foo/bar` | `Si`, `-Si` | Show release metadata | -| `grel list` | `Q`, `-Q` | List installed packages | -| `grel remove foo/bar` | `R`, `-R` | Uninstall | +| `grel list` | `Q`, `-Q` | List installed packages (shows status) | +| `grel remove foo/bar` | `R`, `-R` | Uninstall & clean DB | | `grel clean` | `Sc`, `-Sc` | Purge artifact/metadata cache | -| `grel path add` | - | Inject `bin/` dir into shell/Windows PATH | +| `grel path add` | - | Generate shell/registry snippets for `$PATH` | +| `grel migrate old/path new/path` | - | Remap repo path in state DB | -**UX Features:** -- `tabled` for search/list output -- `indicatif::MultiProgress` for parallel downloads -- `owo-colors` for status: ๐ŸŸข โœ…, ๐ŸŸก โš ๏ธ, ๐Ÿ”ด โŒ, ๐Ÿ”ต โ„น๏ธ -- `--json` for machine-readable output -- `--noconfirm` / `-y` skips all prompts +### Pure CLI Prompt (Artix/Pacman Compatible) +``` +:: 3 compatible asset(s) found for linux/x86_64 + 1) foo-1.0.0-linux-x86_64.tar.gz | 12.4 MB | tar.gz (default) + 2) foo-1.0.0-linux-x86_64-musl.tar.gz | 10.2 MB | tar.gz + 3) foo-1.0.0-linux-amd64.tar.gz | 11.8 MB | tar.gz +:: Select asset to download [1]: _ +``` +- Accepts `1`โ€“`3` or `Enter` (selects `[1]`) +- Invalid input โ†’ `:: Invalid selection. Enter a number [1-3]: ` +- `--noconfirm` / `-y` โ†’ skips prompt, selects `[1]` +- **Non-Interactive Fallback:** `!std::io::stdin().is_terminal()` โ†’ auto-selects `[1]`, prints `โ„น๏ธ Non-interactive mode. Using: ` to `stderr`, never blocks CI/pipes. --- -## 5. PATH Integration & Post-Install Behavior -**Problem:** User-space installs aren't in `$PATH` by default. Pacman installs globally (`/usr/bin`). +## 4. Asset Resolution Pipeline (Deterministic) +**No scoring. No fuzzy logic.** Strict filtering โ†’ transparent sorting โ†’ explicit selection. -**Solution:** Dedicated `bin/` directory + explicit shell integration. +### Step 1: Tokenization +Filenames are split on non-alphanumeric characters. Tokens are matched against case-insensitive alias maps for `Os` and `Arch`. +```rust +// foo-v1.2.3-win64-setup.exe โ†’ Os::Windows, Arch::X86_64 +// bar_1.0.0_linux_amd64.tar.gz โ†’ Os::Linux, Arch::X86_64 +// app.Darwin.arm64.zip โ†’ Os::MacOS, Arch::Aarch64 ``` -~/.local/share/grel/ (Linux) โ†’ bin/ -%LOCALAPPDATA%\grel\ (Windows) โ†’ bin\ +Unknown tokens fall back to `Os::Unknown("...")` or `Arch::Unknown("...")` โ†’ never crash, just filter out later. + +### Step 2: Strict Filtering +Assets are filtered in fixed order. Failure at any step โ†’ discard. +```rust +1. OS matches target OR is Unknown +2. Arch matches priority list OR fallback allowed (32-bit on 64-bit) +3. Format NOT in `ignore_formats` (deb/rpm/msi/dmg disabled by default) ``` -**Implementation:** -1. `grel sync` extracts binaries to `~/.local/share/grel/bin/` (or equivalent) -2. `grel path add` detects shell (`$SHELL`, `$PROFILE`) and appends: - ```bash - export PATH="$HOME/.local/share/grel/bin:$PATH" # bash/zsh - $env:Path = "$env:LOCALAPPDATA\grel\bin;" + $env:Path # PowerShell - ``` -3. Windows: Uses registry `HKCU\Environment\Path` + broadcasts `WM_SETTINGCHANGE` -4. First-run hint: `๐Ÿ’ก Run grel path add to enable global command access` -5. **Why not `/usr/local/bin`?** Avoids `sudo`, respects XDG, prevents system package conflicts. +### Step 3: Deterministic Sorting +Remaining assets sorted by explicit cascade: +1. `arch_priority` index (lower = better) +2. `prefer_formats` index +3. Lexicographic filename +4. Size descending (tie-breaker) + +### Step 4: Selection +- `0` assets โ†’ `โŒ No compatible assets. Run grel sync --list-assets` +- `1` asset โ†’ Auto-select, log `โ„น๏ธ Selected: ` +- `>1` assets โ†’ Pure CLI numbered prompt (see ยง3) + +--- + +## 5. Platform Enums & Alias Mapping +Strongly-typed, exhaustive, infallible parsing. +```rust +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Os { Linux, Windows, MacOS, FreeBSD, Android, iOS, Unknown(String) } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Arch { X86_64, Aarch64, I686, ArmV7, ArmV6, Riscv64, S390x, PowerPC64, Unknown(String) } +``` +- `FromStr` implemented with `to_lowercase()` + alias table +- `serde` ready for TOML config +- `Display` outputs canonical names for DB/CLI consistency --- ## 6. Configuration vs State Management -**Strict Separation:** -| Layer | Format | Location | Purpose | -|-------|--------|----------|---------| -| **User Config** | `config.toml` | `~/.config/grel/` (Linux) / `%APPDATA%\grel\` (Win) | Proxy, tokens, concurrency, asset preferences | -| **Program State** | `state.sqlite` | `~/.local/share/grel/db/` | Installed packages, IP cache, ETags, last-checked | -| **Artifact Cache** | Files | `~/.cache/grel/artifacts/` | Downloaded archives (LRU evicted, 2GB default) | +**Strict separation.** Humans edit TOML. Machines manage SQLite. -**`config.toml` Example:** +### `~/.config/grel/config.toml` ```toml [general] version = 1 @@ -149,106 +140,118 @@ max_concurrent = 4 proxy = "http://127.0.0.1:7890" [assets] -default_selection_policy = "largest" # first | largest | preferred_format -prefer_formats = ["*.tar.gz", "*.zip", "*.exe"] -ignore_patterns = ["*source*", "*.deb", "*.rpm"] +ignore_formats = ["*.deb", "*.rpm", "*.msi", "*.dmg", "*.pkg", "*.AppImage"] +prefer_formats = ["*.tar.gz", "*.tar.xz", "*.zip", "*.exe"] +arch_priority = ["x86_64", "aarch64", "x86", "armv7"] +fallback_to_32bit = true +prefer_musl = false + +[migrations] +"legacy/old-tool" = "new-org/old-tool" [upgrade] check_interval_hours = 6 max_parallel_checks = 10 ``` -**Migrations:** -- Config: `version` field in TOML. On load, `grel-config` runs migration functions (rename keys, add defaults, warn on breaking changes). -- SQLite: `sqlx migrate` with timestamped files. Applied automatically on startup. Zero-downtime schema upgrades. +### `state.sqlite` Schema (`installed` table) +```sql +CREATE TABLE installed ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + forge TEXT NOT NULL, -- 'github', 'gitlab', or self-hosted base URL + owner TEXT NOT NULL, + repo TEXT NOT NULL, + version TEXT NOT NULL, -- Strict semver + asset_filename TEXT NOT NULL, -- Exact remote filename used + checksum TEXT, + install_path TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'orphaned', 'migrated')), + orphaned_at INTEGER, -- Unix timestamp when marked unreachable + last_checked INTEGER, -- For smart polling intervals + installed_at INTEGER DEFAULT (strftime('%s', 'now')) +); +CREATE UNIQUE INDEX idx_pkg_unique ON installed(forge, owner, repo); +CREATE INDEX idx_status ON installed(status); +``` --- -## 7. Asset Resolution & Selection Pipeline -Releases contain multiple formats. `grel` uses **deterministic tiered matching** + **interactive fallback**. +## 7. `-Syu` Upgrade Logic & Edge Cases +### 7.1 Filename Change Detection +Authors often rename assets (`x86_64` โ†’ `amd64`, `.tar.gz` โ†’ `.tar.xz`). +1. Load stored `asset_filename` from DB +2. Resolve new release โ†’ filter โ†’ sort โ†’ get `new_best` +3. If `new_best.filename != stored.filename`: + ``` + โš ๏ธ Remote asset renamed: old-name.tar.gz โ†’ new-name.tar.xz + โ„น๏ธ Proceeding with update... + ``` +4. Download โ†’ verify โ†’ extract โ†’ **update DB record** with new filename. Proceeds safely. -### Tiered Matching (Order Matters) -1. **Exact:** `{repo}-{version}-{os}-{arch}.{fmt}` -2. **OS+Arch:** `*{os}*{arch}*.{fmt}` -3. **OS Only:** `*{os}*.{fmt}` -4. **Universal Archive:** `*.{tar.gz,tar.xz,zip}` -5. **Universal Binary:** `*.{exe,elf,dll,dylib}` +### 7.2 Repository Rename / 404 Handling +1. Provider fetch โ†’ `404` or unreachable +2. Mark `status = 'orphaned'`, set `orphaned_at = now()` +3. Skip in future `-Syu` runs +4. Warn user: + ``` + โš ๏ธ Package unreachable: foo/bar (HTTP 404) + โ„น๏ธ Skipped. Run `grel migrate foo/bar new/path` or `grel remove foo/bar` + ``` +5. **No auto-migration.** Explicit user action required. -### Checksum & Signature Auto-Pairing -If main asset is `foo-1.0.0-linux-amd64.tar.gz`, `grel` searches for: -- `foo-1.0.0-linux-amd64.tar.gz.sha256` -- `sha256sums.txt` (parses line matching filename) -Automatically downloads & verifies before extraction. - -### Interactive Selection (Pacman/Artix Style) -- Uses `dialoguer::Select` with numbered list -- Default marker: `(default)` shown next to highest-priority match -- `--noconfirm` / `-y` skips prompt, uses default -- **TTY Detection:** If not interactive (CI/pipes), auto-selects default + prints `โ„น๏ธ Non-interactive mode. Using default: ` to stderr -- Timeout fallback (configurable): auto-selects default if no input within `prompt_timeout_secs` - -**CLI Overrides:** +### 7.3 Orphaned Visibility ```bash -grel sync foo/bar --asset custom.exe -grel sync foo/bar --platform linux/aarch64 -grel sync foo/bar --list-assets # Dry-run table +$ grel list +:: Installed packages (3 active, 1 orphaned) + ๐ŸŸข bar/fuzz 1.2.3 linux/x86_64 tar.gz + ๐ŸŸข foo/tool 0.9.1 windows/amd64 exe + ๐ŸŸก legacy/old-proj 2.0.0 linux/x86_64 tar.gz [orphaned since 2024-05-01] ``` --- ## 8. Networking, Proxy & DNS/IP Caching -### Proxy Support -- Native via `reqwest::Proxy::all()`, `http()`, `https()` -- Priority: `--proxy` > `GREL_PROXY` > `http_proxy`/`all_proxy` > `config.toml` -- Supports `http://user:pass@host`, `socks5://...`, `socks5h://...` - -### DNS/IP Caching -โš ๏ธ **No hardcoded IPs.** CDNs rotate frequently. Instead: -1. Resolve `A/AAAA` via `hickory-resolver` -2. Parallel `TcpStream::connect` probes to all IPs on `443` -3. Store fastest IP + RTT in SQLite with TTL (default `300s`) -4. Use `reqwest::ClientBuilder::resolve(host, ip)` for routing -5. Background refresh thread evicts stale entries, pre-warms hot domains +- **Proxy Priority:** `--proxy` > `GREL_PROXY` env > `http_proxy`/`all_proxy` > `config.toml` +- **DNS/IP Cache:** + 1. Resolve `A/AAAA` via `hickory-resolver` + 2. Parallel `TcpStream::connect` probes on `443` + 3. Store fastest IP + RTT in SQLite with `300s` TTL + 4. `reqwest::ClientBuilder::resolve(host, ip)` forces routing +- **Background Refresh:** Idle task pre-warms hot domains, evicts stale entries --- -## 9. Rate Limiting & `-Syu` Optimization -GitHub allows 60 req/hr unauthenticated, 5000/hr authenticated. `grel` handles this gracefully: - -1. **ETag Caching:** `If-None-Match` โ†’ `304 Not Modified` **does not count** against rate limits. Cached ETags stored in `state.sqlite`. -2. **Smart Polling:** Only recheck packages where `last_checked < now() - check_interval_hours`. -3. **Rate Limit Awareness:** Parse `X-RateLimit-Remaining` & `X-RateLimit-Reset`. If `< 50`, sleep until reset or queue remaining checks. -4. **Auth Token:** Strongly recommended. `grel` prompts on first run: `๐Ÿ”‘ Set GREL_GITHUB_TOKEN for 5000 req/hr`. -5. **`--no-api` Fallback:** Skips remote checks, uses local manifest + cache timestamps. -6. **Parallel Batch Checks:** `max_parallel_checks` limits concurrent API calls to avoid hitting burst limits. - -**`-Syu` Flow:** -``` -Load state โ†’ Filter stale โ†’ Batch API (ETag) โ†’ Parse 200/304 โ†’ Build diff โ†’ Prompt โ†’ Download โ†’ Verify โ†’ Update -``` +## 9. Rate Limiting & ETag Optimization +GitHub: 60/hr unauth, 5000/hr auth. `grel` handles gracefully: +1. **ETag Caching:** `If-None-Match` โ†’ `304 Not Modified` **free**. Cached in DB. +2. **Smart Polling:** Only checks packages where `last_checked < now() - check_interval_hours` +3. **Rate Limit Parsing:** Reads `X-RateLimit-Remaining`/`Reset`. If `< 50` โ†’ sleep/queue. +4. **Auth Prompt:** First run suggests `GREL_GITHUB_TOKEN` for 5000/hr. +5. **`--no-api` Fallback:** Skips remote checks, uses local timestamps only. --- -## 10. Cross-Platform Implementation Notes -| Aspect | Linux | Windows | Implementation | -|--------|-------|---------|----------------| -| Paths | `~/.local/share/grel` | `%LOCALAPPDATA%\grel` | `directories::ProjectDirs` | -| Archives | `tar` + `zstd`/`xz`/`gz` | `zip` + `tar` | Feature-gated extraction | -| Exec Perms | `0o755` | `FILE_ATTRIBUTE_ARCHIVE` | `cfg(unix)` vs `cfg(windows)` | -| Path Safety | Reject `..`, absolute paths | Same | `sanitize-filename` + strict validation | -| Executable Suffix | None | `.exe` | Auto-append if `cfg(windows)` & missing | -| Shell Integration | `.bashrc`, `.zshrc`, fish | `$PROFILE`, Registry | `grel path add` | +## 10. PATH Integration & Post-Install +- Installs to `~/.local/share/grel/bin/` (Linux) / `%LOCALAPPDATA%\grel\bin\` (Windows) +- `grel path add` prints exact shell/registry snippets: + ```bash + export PATH="$HOME/.local/share/grel/bin:$PATH" + ``` + ```powershell + [Environment]::SetEnvironmentVariable("Path", "$env:LOCALAPPDATA\grel\bin;$env:Path", "User") + ``` +- First run: `๐Ÿ’ก Run: grel path add to enable global command access` +- XDG-compliant, no `sudo`, no system conflicts. --- -## 11. Security & Reliability Guarantees -- โœ… **TLS:** `rustls` only (no native OpenSSL) -- โœ… **Checksums:** Verify `sha256` before extraction -- โœ… **Archive Safety:** Reject `..`, absolute paths, symlinks to outside dest -- โœ… **Atomic Installs:** Download to temp dir โ†’ verify โ†’ `rename` โ†’ register in DB -- โœ… **No Auto-Execute:** Binaries installed but not run unless invoked by user -- โœ… **Token Security:** Loaded from env/config, never logged or serialized to stdout -- โœ… **Graceful Degradation:** Network errors โ†’ retry with backoff, rate limits โ†’ sleep/queue, missing assets โ†’ clear error + suggestions +## 11. Security & Reliability +- โœ… TLS: `rustls` only (no native OpenSSL) +- โœ… Checksums: `sha256` verification before extraction +- โœ… Archive safety: Reject `..`, absolute paths, external symlinks +- โœ… Atomic installs: Temp dir โ†’ verify โ†’ `rename` โ†’ DB update +- โœ… No auto-exec: Binaries installed but not run unless invoked +- โœ… Graceful degradation: Network errors โ†’ retry/backoff, rate limits โ†’ sleep/queue, missing assets โ†’ clear error + suggestions --- @@ -259,11 +262,10 @@ tokio = { version = "1", features = ["full"] } futures = "0.3" async-trait = "0.1" -# CLI & UX +# CLI & UX (Pure CLI, no TUI) clap = { version = "4", features = ["derive", "wrap_help", "string"] } indicatif = { version = "0.17", features = ["tokio"] } tracing-indicatif = "0.3" -dialoguer = "0.11" tabled = "0.16" owo-colors = "4" @@ -288,23 +290,23 @@ figment = { version = "0.10", features = ["toml", "env"] } semver = "1" url = "2" chrono = { version = "0.4", features = ["serde"] } +regex = "1" serde = { version = "1", features = ["derive"] } anyhow = "1" thiserror = "2" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } ``` -*Note: Rust 1.70+ required for `std::io::IsTerminal` (replaces `atty`).* --- ## 13. Testing & CI Strategy -- **Unit:** Parsing, version comparison, cache TTL math, asset matching tiers -- **Mocked Providers:** `wiremock` for API responses (200, 304, 403, 429, 500) -- **Integration:** Download test fixtures โ†’ extract โ†’ verify structure/checksums +- **Unit:** Token extraction from 20+ messy filenames, filter/sort determinism, config parsing, migration logic +- **Mocked Providers:** `wiremock` for 200/304/403/429/500, rate limit header injection +- **Integration:** Download fixtures โ†’ extract โ†’ verify checksums & paths, `grel migrate` DB state changes +- **Non-Interactive:** `echo "" | grel sync foo/bar` must not hang, must log to stderr - **Cross-Platform CI:** `ubuntu-latest`, `windows-latest` (GitHub Actions) -- **Performance:** `criterion` for DNS resolution + parallel download throughput -- **Fuzzing:** `cargo-fuzz` on `PackageRef` parser + archive path validation -- **Non-Interactive Tests:** `echo "" | grel sync foo/bar` must not hang +- **Performance:** `criterion` for DNS/IP cache + parallel download throughput +- **Fuzzing:** `cargo-fuzz` on `AssetTokens::from_filename()` + archive path validation ---