Files
blog-post-backup/designs/grel-rs/TECHNICAL_DESIGN.md

313 lines
13 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 📘 `grel-rs` Technical Design Document (v3.0)
**Binary:** `grel` | **Repository:** `grel-rs`
**Target Platforms:** Linux, Windows (macOS optional)
**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 pipeline, supports HTTP/SOCKS5 proxies, caches optimal DNS endpoints, downloads assets in parallel, and delivers a transparent, scriptable, pacman-compatible UX.
**Key Requirements Met:**
- ✅ 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)
---
## 2. Workspace Architecture
```
grel-rs/
├── Cargo.toml # Workspace root (resolver = "2")
├── crates/
│ ├── 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, parallel downloader
│ ├── grel-cache/ # SQLite state, IP cache, artifact storage, TTL eviction
│ └── grel-config/ # Layered config, migrations, asset priority matrices
├── tests/ # Integration, mock servers, e2e fixtures
└── scripts/ # CI, release, benchmark helpers
```
| 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. 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 |
| `grel sync foo/bar@1.2.3` | `-S` | Pin version |
| `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 (shows status) |
| `grel remove foo/bar` | `R`, `-R` | Uninstall & clean DB |
| `grel clean` | `Sc`, `-Sc` | Purge artifact/metadata cache |
| `grel path add` | - | Generate shell/registry snippets for `$PATH` |
| `grel migrate old/path new/path` | - | Remap repo path in state DB |
### 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: <filename>` to `stderr`, never blocks CI/pipes.
---
## 4. Asset Resolution Pipeline (Deterministic)
**No scoring. No fuzzy logic.** Strict filtering → transparent sorting → explicit selection.
### 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
```
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)
```
### 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: <filename>`
- `>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.** Humans edit TOML. Machines manage SQLite.
### `~/.config/grel/config.toml`
```toml
[general]
version = 1
max_concurrent = 4
proxy = "http://127.0.0.1:7890"
[assets]
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
```
### `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. `-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.
### 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.
### 7.3 Orphaned Visibility
```bash
$ 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 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 & 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. 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
- ✅ 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
---
## 12. Dependency Matrix
```toml
# Core & Async
tokio = { version = "1", features = ["full"] }
futures = "0.3"
async-trait = "0.1"
# CLI & UX (Pure CLI, no TUI)
clap = { version = "4", features = ["derive", "wrap_help", "string"] }
indicatif = { version = "0.17", features = ["tokio"] }
tracing-indicatif = "0.3"
tabled = "0.16"
owo-colors = "4"
# Network & Proxy
reqwest = { version = "0.12", features = ["rustls-tls", "json", "socks", "stream"] }
hickory-resolver = "0.24"
dashmap = "6"
# Cache & Storage
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite"] }
sha2 = "0.10"
directories = "5"
tar = "0.4"
zip = "2"
zstd = "0.13"
xz2 = "0.1"
bzip2 = "0.5"
sanitize-filename = "0.5"
# Config & Utils
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"] }
```
---
## 13. Testing & CI Strategy
- **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/IP cache + parallel download throughput
- **Fuzzing:** `cargo-fuzz` on `AssetTokens::from_filename()` + archive path validation
---