diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..eed25d6b8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,200 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**rtk (Rust Token Killer)** is a high-performance CLI proxy that minimizes LLM token consumption by filtering and compressing command outputs. It achieves 60-90% token savings on common development operations through smart filtering, grouping, truncation, and deduplication. + +This is a fork with critical fixes for git argument parsing and modern JavaScript stack support (pnpm). + +## Development Commands + +### Build & Run +```bash +# Development build +cargo build + +# Release build (optimized) +cargo build --release + +# Run directly +cargo run -- + +# Install locally +cargo install --path . +``` + +### Testing +```bash +# Run all tests +cargo test + +# Run specific test +cargo test + +# Run tests with output +cargo test -- --nocapture + +# Run tests in specific module +cargo test :: +``` + +### Linting & Quality +```bash +# Check without building +cargo check + +# Format code +cargo fmt + +# Run clippy lints +cargo clippy + +# Check all targets +cargo clippy --all-targets +``` + +### Package Building +```bash +# Build DEB package (Linux) +cargo install cargo-deb +cargo deb + +# Build RPM package (Fedora/RHEL) +cargo install cargo-generate-rpm +cargo build --release +cargo generate-rpm +``` + +## Architecture + +### Core Design Pattern + +rtk uses a **command proxy architecture** with specialized modules for each output type: + +``` +main.rs (CLI entry) + → Clap command parsing + → Route to specialized modules + → tracking.rs (SQLite) records token savings +``` + +### Key Architectural Components + +**1. Command Modules** (src/*_cmd.rs, src/git.rs, src/container.rs) +- Each module handles a specific command type (git, grep, diff, etc.) +- Responsible for executing underlying commands and transforming output +- Implement token-optimized formatting strategies + +**2. Core Filtering** (src/filter.rs) +- Language-aware code filtering (Rust, Python, JavaScript, etc.) +- Filter levels: `none`, `minimal`, `aggressive` +- Strips comments, whitespace, and function bodies (aggressive mode) +- Used by `read` and `smart` commands + +**3. Token Tracking** (src/tracking.rs) +- SQLite-based persistent storage (~/.local/share/rtk/tracking.db) +- Records: original_cmd, rtk_cmd, input_tokens, output_tokens, savings_pct +- 90-day retention policy with automatic cleanup +- Powers the `rtk gain` analytics command + +**4. Configuration System** (src/config.rs, src/init.rs) +- Manages CLAUDE.md initialization (global vs local) +- Reads ~/.config/rtk/config.toml for user preferences +- `rtk init` command bootstraps LLM integration + +### Command Routing Flow + +All commands follow this pattern: +```rust +main.rs:Commands enum + → match statement routes to module + → module::run() executes logic + → tracking::track_command() records metrics + → Result<()> propagates errors +``` + +### Critical Implementation Details + +**Git Argument Handling** (src/git.rs) +- Uses `trailing_var_arg = true` + `allow_hyphen_values = true` to properly handle git flags +- Auto-detects `--merges` flag to avoid conflicting with `--no-merges` injection +- Propagates git exit codes for CI/CD reliability (PR #5 fix) + +**Output Filtering Strategy** +- Compact mode: Show only summary/failures +- Full mode: Available with `-v` verbosity flags +- Test output: Show only failures (90% token reduction) +- Git operations: Ultra-compressed confirmations ("ok ✓") + +**Language Detection** (src/filter.rs) +- File extension-based with fallback heuristics +- Supports Rust, Python, JS/TS, Java, Go, C/C++, etc. +- Tokenization rules vary by language (comments, strings, blocks) + +### Module Responsibilities + +| Module | Purpose | Token Strategy | +|--------|---------|----------------| +| git.rs | Git operations | Stat summaries + compact diffs | +| grep_cmd.rs | Code search | Group by file, truncate lines | +| ls.rs | Directory listing | Tree format, aggregate counts | +| read.rs | File reading | Filter-level based stripping | +| runner.rs | Command execution | Stderr only (err), failures only (test) | +| log_cmd.rs | Log parsing | Deduplication with counts | +| json_cmd.rs | JSON inspection | Structure without values | + +## Fork-Specific Features + +### PR #5: Git Argument Parsing Fix (CRITICAL) +- **Problem**: Git flags like `--oneline`, `--cached` were rejected +- **Solution**: Fixed Clap parsing with proper trailing_var_arg configuration +- **Impact**: All git commands now accept native git flags + +### PR #6: pnpm Support +- **New Commands**: `rtk pnpm list`, `rtk pnpm outdated`, `rtk pnpm install` +- **Token Savings**: 70-90% reduction on package manager operations +- **Security**: Package name validation prevents command injection + +## Testing Strategy + +Tests are embedded in modules using `#[cfg(test)] mod tests`: +- Unit tests validate filtering logic (filter.rs, grep_cmd.rs, etc.) +- Integration tests verify command output transformations (git.rs, runner.rs) +- Security tests ensure proper command sanitization (pnpm validation) + +Run module-specific tests: +```bash +cargo test filter::tests:: +cargo test git::tests:: +cargo test runner::tests:: +``` + +## Dependencies + +Core dependencies (see Cargo.toml): +- **clap**: CLI parsing with derive macros +- **anyhow**: Error handling +- **rusqlite**: SQLite for tracking database +- **regex**: Pattern matching for filtering +- **ignore**: gitignore-aware file traversal +- **colored**: Terminal output formatting +- **serde/serde_json**: Configuration and JSON parsing + +## Build Optimizations + +Release profile (Cargo.toml:31-36): +- `opt-level = 3`: Maximum optimization +- `lto = true`: Link-time optimization +- `codegen-units = 1`: Single codegen for better optimization +- `strip = true`: Remove debug symbols +- `panic = "abort"`: Smaller binary size + +## CI/CD + +GitHub Actions workflow (.github/workflows/release.yml): +- Multi-platform builds (macOS, Linux x86_64/ARM64, Windows) +- DEB/RPM package generation +- Automated releases on version tags (v*) +- Checksums for binary verification diff --git a/Cargo.lock b/Cargo.lock index 26d38e180..0a7da42f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,7 +581,7 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rtk" -version = "0.1.9" +version = "0.2.0" dependencies = [ "anyhow", "chrono", diff --git a/README.md b/README.md index 3f1bf850a..61c214e3a 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,105 @@ FAILED: 2/15 tests 3. **Truncation**: Keeps relevant context, cuts redundancy 4. **Deduplication**: Collapses repeated log lines with counts +## Fork Features + +This fork adds critical fixes and modern JavaScript stack support, validated on production T3 Stack codebases. + +### 🔧 Git Argument Parsing Fix + +**Problem**: Git flags like `--oneline`, `--cached`, `--graph` were rejected as invalid arguments. + +**Solution**: +- Fixed Clap argument parsing with `trailing_var_arg + allow_hyphen_values` +- Auto-detects `--merges` flag to skip `--no-merges` injection +- Propagates git exit codes properly (fixes CI/CD false positives) + +**Now Working**: +```bash +rtk git log --oneline -20 # Compact commit history +rtk git diff --cached # Staged changes only +rtk git log --graph --all # Branch visualization +rtk git status --short # Ultra-compact status +``` + +### 📦 pnpm Support for T3 Stack + +Adds first-class pnpm support with security hardening. + +**Commands**: +```bash +rtk pnpm list # Dependency tree (70% token reduction) +rtk pnpm outdated # Update candidates (80-90% reduction) +rtk pnpm install # Silent success confirmation +``` + +**Token Savings**: +| Command | Standard Output | rtk Output | Reduction | +|---------|----------------|------------|-----------| +| `pnpm list` | ~8,000 tokens | ~2,400 | -70% | +| `pnpm outdated` | ~12,000 tokens | ~1,200-2,400 | -80-90% | +| `pnpm install` | ~500 tokens | ~10 | -98% | + +**Security**: +- Package name validation (prevents command injection) +- Proper error propagation (fixes CI/CD reliability) + +### 🧪 Vitest Test Runner Support + +Modern test runner integration for JavaScript/TypeScript projects. + +**Command**: +```bash +rtk vitest run # Filtered test output (99.6% token reduction) +``` + +**Token Savings** (validated on production codebase): +| Test Suite | Standard Output | rtk Output | Reduction | +|------------|----------------|------------|-----------| +| 250 tests (2 failures) | 102,199 chars | 377 chars | **-99.6%** ✅ | + +**Output Format**: +``` +PASS (10) FAIL (2) + +1. FAIL tests/unit/api/services/activity.test.ts + Error: env.OPENAI_API_KEY accessed on client + at line 73 + +2. FAIL tests/unit/lib/utils/validator.test.ts + should be a readonly array + +Time: 3.05s +``` + +**What's Preserved**: +- ✅ Pass/fail counts +- ✅ Failure details with file paths +- ✅ Error context (line numbers + code snippets) +- ✅ Execution timing + +### 📥 Installation (This Fork) + +**Recommended** until upstream merges these features: + +```bash +# Clone and build +git clone https://github.com/FlorianBruniaux/rtk.git +cd rtk +cargo build --release + +# Install globally +cargo install --path . + +# Or use directly +./target/release/rtk --version +``` + +**Switch to Upstream** (once features are merged): +```bash +cargo install rtk --force +``` + ## Configuration rtk reads from `CLAUDE.md` files to instruct Claude Code to use rtk automatically: diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 000000000..8baad53de --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,683 @@ +# RTK Roadmap - Plan d'Action Complet + +## 🎯 Vue d'Ensemble + +**Mission**: Transformer RTK d'un CLI proxy MVP vers un outil production-ready pour T3 Stack et au-delà. + +**Horizon**: 4 phases sur 12 semaines + +**Critères de Succès**: +- ✅ 70%+ token reduction validé en production (Méthode Aristote) +- ✅ Adoption par 3+ projets/équipes +- ✅ PRs mergées upstream ou fork maintenu comme standard + +--- + +## 📊 État Actuel (Baseline) + +### ✅ Achievements (Phase 0) +- Fork RTK upstream créé +- 3 Issues ouvertes (#2, #3, #4) +- 2 PRs créées: + - PR #5: Git argument parsing fix → CLOSED (merged dans #6) + - PR #6: Git + pnpm support → **OPEN** (5 commits, 429 LOC) +- Branche feat/vitest-support créée (1 commit, 522 LOC) +- Installation validée sur Méthode Aristote +- **43.5K tokens économisés** sur 18 commandes (68.8%) + +### ❌ Gaps Identifiés +- Async/await: 0% du codebase +- Observability: Pas de tracing structuré +- Type safety: Pas de newtypes métier +- Cross-platform: Tests uniquement macOS +- Upstream engagement: Pas de réponse maintainer (1 semaine) + +--- + +## 🚀 Phase 1: Production Readiness (Semaines 1-2) + +**Objectif**: Stabiliser RTK pour usage quotidien sur Méthode Aristote + +### 1.1 Issues Upstream (Priorité: 🔴 CRITIQUE) + +**Issue #2: Git argument parsing bug → RÉSOLU** ✅ +- Status: Résolu par PR #6 +- Action: Monitorer merge de PR #6 + +**Issue #3: T3 Stack support (pnpm + Vitest) → EN COURS** 🔄 +- pnpm: ✅ Implémenté (PR #6) +- Vitest: ✅ Implémenté (feat/vitest-support branch) +- Action: Tester Vitest sur Aristote, créer PR #7 + +**Issue #4: grep/ls bugs → TODO** ⏳ +- Priorité: MEDIUM (pas bloquant) +- Effort: 1-2 jours +- Action: Repro bugs sur Aristote, fix + tests + +### 1.2 Vitest Support - Finalisation (Priorité: 🟡 HIGH) + +**Status**: Module implémenté (298 LOC + tests), non testé en production + +**Actions restantes**: +1. ✅ **Test sur Aristote** (1h) + ```bash + cd /Users/florianbruniaux/Sites/MethodeAristote/app + pnpm test | tee /tmp/vitest-raw.txt + rtk vitest run | tee /tmp/vitest-rtk.txt + wc -c /tmp/vitest-*.txt # Compare token counts + ``` + +2. **Mesurer économies réelles** (30 min) + - Target: 90% reduction (10.5K → 1K chars) + - Valider format: `PASS (n) FAIL (n) + failures + timing` + +3. **Documenter dans README** (1h) + - Ajouter section Vitest + - Exemples before/after + - Token savings metrics + +4. **Créer PR #7** (si PR #6 pas mergée après 2 semaines) + - Vitest standalone OU + - Combiner avec #6 si maintainer responsive + +**Estimation**: 3-4h total + +### 1.3 Documentation & Onboarding (Priorité: 🟡 MEDIUM) + +**Objectif**: Faciliter adoption par d'autres équipes + +**Livrables**: +1. **README.md exhaustif** (2h) + - Quick start (3 étapes max) + - Tous les use cases T3 Stack + - Troubleshooting FAQ + - Benchmarks visuels (graphes token savings) + +2. **CONTRIBUTING.md** (1h) + - Guidelines pour PRs + - Architecture overview + - Testing strategy + - Code review checklist + +3. **Video demo** (optionnel, 2h) + - Screencast 5-10 min + - Installation → Usage → Savings + - Publier sur YouTube + embed README + +**Estimation**: 5h total + +### 1.4 Testing & Quality (Priorité: 🟡 MEDIUM) + +**Objectif**: Confiance pour déployer chez d'autres + +**Actions**: +1. **Cross-platform validation** (2h) + - macOS: ✅ OK + - Linux: À tester (via Docker) + - Windows: À tester (via WSL ou VM) + +2. **Integration tests** (3h) + - Tester sur 2-3 projets T3 Stack publics + - Vérifier: next, vitest, pnpm, prisma + - Documenter edge cases + +3. **CI/CD enhancement** (2h) + - Ajouter tests dans GitHub Actions + - Test matrix: [macOS, Linux, Windows] + - Clippy lints + cargo fmt check + +**Estimation**: 7h total + +--- + +## 🎯 Phase 2: Upstream Engagement (Semaines 3-4) + +**Objectif**: Merger PRs upstream OU établir fork comme standard + +### 2.1 Stratégie de Merge (Priorité: 🔴 CRITICAL) + +**Scénario A: PR #6 mergée rapidement** ✅ +- Action: Créer PR #7 (Vitest) dès merge de #6 +- Timeframe: 1 semaine après merge #6 + +**Scénario B: Pas de réponse après 2 semaines** ⚠️ +- Action: Pivot vers fork maintenu indépendamment +- Communication: + ```markdown + ## Fork Status + + This fork contains critical fixes and modern JS stack support: + - Git argument parsing (upstream PR #6 pending) + - pnpm support for T3 Stack + - Vitest test runner integration + + **Use this fork** until upstream merges these features. + + Installation: `cargo install --git https://github.com/FlorianBruniaux/rtk` + ``` + +**Scénario C: Maintainer demande des changements** 🔄 +- Action: Appliquer feedback rapidement (< 48h) +- Priorité: Maintenir momentum + +### 2.2 Community Building (Priorité: 🟢 LOW) + +**Objectif**: Créer traction pour adoption + +**Actions**: +1. **Blog post technique** (4h) + - Titre: "Reducing LLM Token Usage by 70% with RTK on T3 Stack" + - Contenu: Problem → Solution → Results → Code + - Publier: dev.to, Medium, X (Twitter) + +2. **Engagement Reddit/HN** (2h) + - Post sur r/rust, r/typescript, r/nextjs + - Show HN si traction forte + - Focus: Real metrics, production usage + +3. **Issue templates upstream** (1h) + - Faciliter contributions d'autres users + - Bug report, feature request, support + +**Estimation**: 7h total + +--- + +## 🎯 Phase 3: Advanced Features (Semaines 5-8) + +**Objectif**: Étendre RTK au-delà du MVP + +### 3.1 Architecture Moderne (Priorité: 🟡 MEDIUM) + +**3.1.1 Async/Await Refactor** (Priorité: 🔴 HIGH si LLM integration) + +**Problème actuel**: +```rust +// Blocking sync code +let output = Command::new("git").output()?; +``` + +**Target**: +```rust +#[tokio::main] +async fn main() -> Result<()> { + let output = tokio::process::Command::new("git") + .output() + .await?; +} +``` + +**Bénéfices**: +- Parallel command execution +- Future LLM API integration (`rtk ask "explain this"`) +- Streaming responses + +**Effort**: 2-3 semaines (refactor complet) + +**Décision**: ⚠️ **Attendre validation métier** +- Si RTK reste CLI proxy → Pas nécessaire +- Si évolution vers agent LLM → Indispensable + +**Action immédiate**: Prototyper branch `feat/async-refactor` sans merger + +### 3.1.2 Observability avec Tracing** (Priorité: 🟡 MEDIUM) + +**Problème actuel**: +```rust +if verbose > 0 { + eprintln!("pnpm list (filtered):"); +} +``` + +**Target**: +```rust +use tracing::{info, instrument}; + +#[instrument(skip(args))] +fn run_pnpm_list(args: &[String]) -> Result<()> { + info!(command = "pnpm list", "Executing"); + // ... + info!( + input_tokens = %input, + output_tokens = %output, + savings_pct = %savings, + "Command completed" + ); +} +``` + +**Bénéfices**: +- Structured logs (JSON export) +- Performance debugging +- Production monitoring + +**Effort**: 1 semaine + +**Actions**: +1. Ajouter `tracing` + `tracing-subscriber` deps +2. Replace `eprintln!` par `tracing::*` macros +3. Add `--log-format json` flag +4. Export to OpenTelemetry (optionnel) + +### 3.1.3 Type Safety avec Newtypes** (Priorité: 🟢 LOW) + +**Problème actuel**: +```rust +pub fn track(original_cmd: &str, rtk_cmd: &str, ...) +// Facile de confondre les deux +``` + +**Target**: +```rust +#[derive(Debug)] +struct OriginalCommand(String); + +#[derive(Debug)] +struct RtkCommand(String); + +pub fn track( + original: OriginalCommand, + rtk: RtkCommand, + savings: TokenSavings +) +``` + +**Bénéfices**: +- Compile-time safety +- Self-documenting code +- Refactoring confidence + +**Effort**: 2-3 jours + +**Actions**: +1. Créer `types.rs` module +2. Define newtypes métier +3. Migrate incrementally (one module at a time) + +### 3.2 Features Utilisateurs (Priorité: 🟡 MEDIUM) + +**3.2.1 Config File Support** (Priorité: 🟢 LOW) + +**Use case**: Personnaliser filtres par projet + +**Target** (`~/.config/rtk/config.toml`): +```toml +[filters] +git_status_max_files = 50 +pnpm_list_max_depth = 2 + +[tokens] +estimate_multiplier = 4 # 1 char ≈ 4 tokens + +[integrations] +export_format = "json" +``` + +**Effort**: 2-3 jours + +**Actions**: +1. Extend `config.rs` module +2. Add `--config` flag +3. Merge with existing hardcoded defaults +4. Add `rtk config show` command + +**3.2.2 Watch Mode** (Priorité: 🟢 LOW) + +**Use case**: Monitor file changes + auto-execute + +**Target**: +```bash +rtk watch "pnpm test" --on-change "src/**/*.ts" +# Re-runs tests on file save, filtered output +``` + +**Effort**: 1 semaine (needs `notify` crate) + +**Décision**: ⚠️ **Bas ROI** - Existe déjà dans test runners + +**3.2.3 LLM Integration** (Priorité: 🔴 HIGH si adoption forte) + +**Use case**: Ask questions about codebase + +**Target**: +```bash +rtk ask "Explain this git log" +rtk ask "What changed in last commit?" --context "git show" +``` + +**Architecture**: +```rust +use anthropic_sdk::Client; + +async fn ask_command(prompt: &str, context_cmd: Option<&str>) { + let context = if let Some(cmd) = context_cmd { + execute_and_filter(cmd).await? + } else { + String::new() + }; + + let response = client.messages() + .create(MessagesRequest { + model: "claude-opus-4-5", + messages: vec![Message { + role: "user", + content: format!("{}\n\nContext:\n{}", prompt, context), + }], + max_tokens: 1000, + }) + .await?; + + println!("{}", response.content); +} +``` + +**Effort**: 2-3 semaines (requires async refactor) + +**Bénéfices**: +- RTK devient agent, pas juste proxy +- Killer feature vs upstream + +**Risques**: +- Needs API keys (friction onboarding) +- Costs money (user concern) +- Async refactor mandatory + +**Décision**: ⚠️ **Phase 4** - Après validation adoption RTK classique + +--- + +## 🎯 Phase 4: Ecosystem & Scale (Semaines 9-12) + +**Objectif**: RTK comme standard T3 Stack tooling + +### 4.1 Package Distribution (Priorité: 🔴 CRITICAL) + +**4.1.1 Homebrew Tap** (macOS users) + +**Actions**: +1. Create `homebrew-tap` repo +2. Add Formula (déjà existe: `Formula/rtk.rb`) +3. Automate releases via GitHub Actions +4. Test: `brew install florianbruniaux/tap/rtk` + +**Effort**: 1 jour + +**4.1.2 Binary Releases** (multi-platform) + +**Target platforms**: +- macOS (Intel + Apple Silicon) +- Linux (x86_64 + ARM64) +- Windows (x86_64) + +**Actions**: +1. Enhance `.github/workflows/release.yml` +2. Cross-compile with `cross` tool +3. Upload to GitHub Releases +4. Add checksums (SHA256) + +**Effort**: 1-2 jours (déjà 80% fait) + +**4.1.3 npm Package** (optionnel, JavaScript devs) + +**Use case**: `npx rtk git status` sans installer Rust + +**Implementation**: +```json +{ + "name": "@rtk/cli", + "bin": { + "rtk": "./bin/rtk" + }, + "postinstall": "node scripts/download-binary.js" +} +``` + +**Effort**: 2-3 jours + +**Décision**: ⚠️ **Évaluer demand** - Peut être overkill + +### 4.2 IDE Integrations (Priorité: 🟡 MEDIUM) + +**4.2.1 VSCode Extension** + +**Features**: +- Inline token savings preview +- Command palette: `RTK: Run Command` +- Status bar: Token savings today +- Settings: Configure filters + +**Effort**: 1-2 semaines (TypeScript + Extension API) + +**4.2.2 Cursor/Windsurf Integration** + +**Use case**: Native RTK support in AI IDEs + +**Actions**: +1. Propose integration to Cursor team +2. Provide SDK/API for tool invocation +3. Documentation for integration + +**Effort**: 1 semaine (mostly coordination) + +### 4.3 Community & Support (Priorité: 🟢 LOW) + +**4.3.1 Documentation Site** (optionnel) + +**Stack**: Nextra (Next.js docs framework) + +**Sections**: +- Getting Started +- Command Reference +- Integration Guides (T3 Stack, Remix, etc.) +- FAQ +- Blog + +**Effort**: 1 semaine + +**URL**: `rtk-docs.vercel.app` OU GitHub Pages + +**4.3.2 Discord Community** (optionnel) + +**Use case**: User support, feature requests + +**Effort**: Setup 1h, moderation ongoing + +**Décision**: ⚠️ **Seulement si adoption >100 users** + +--- + +## 🎓 Skills Rust à Développer + +**Basé sur analyse guide "Rust + Claude AI"** + +### Niveau 1: Fondations (Semaines 1-2) + +**Async/Await + Tokio** (Priorité: 🔴 HIGH) +- Resource: [Rust Async Book](https://rust-lang.github.io/async-book/) +- Projet: Refactor `git.rs` vers async +- Validation: Parallel `rtk git status && rtk git log` + +**Tracing/Observability** (Priorité: 🟡 MEDIUM) +- Resource: [tracing crate docs](https://docs.rs/tracing) +- Projet: Add structured logging to all commands +- Validation: `rtk --log-format json | jq` + +### Niveau 2: Intermédiaire (Semaines 3-4) + +**Error Handling Patterns** (Priorité: 🟡 MEDIUM) +- Resource: [thiserror + anyhow guide](https://nick.groenen.me/posts/rust-error-handling/) +- Projet: Create custom error types with context +- Validation: Error messages 100% actionables + +**Type Safety Patterns** (Priorité: 🟢 LOW) +- Resource: [Rust newtypes pattern](https://doc.rust-lang.org/rust-by-example/generics/new_types.html) +- Projet: Introduce `Command`, `TokenCount` newtypes +- Validation: Compile errors on type confusion + +### Niveau 3: Avancé (Semaines 5-8) + +**Production Deployment** (Priorité: 🟡 MEDIUM) +- Resource: [Building reliable systems in Rust](https://www.shuttle.rs/blog) +- Projet: Health checks, metrics, graceful shutdown +- Validation: Deploy as systemd service + +**Cross-platform Development** (Priorité: 🟡 MEDIUM) +- Resource: [cross tool](https://github.com/cross-rs/cross) +- Projet: Windows support (path handling, commands) +- Validation: CI tests on Windows/Linux/macOS + +### Niveau 4: Expert (Semaines 9-12) + +**LLM API Integration** (Priorité: 🔴 HIGH si feature activée) +- Resource: [Claude Agent SDK](https://lib.rs/crates/claude-agent-sdk) +- Projet: `rtk ask` command with streaming +- Validation: Interactive Q&A with codebase context + +**Performance Optimization** (Priorité: 🟢 LOW) +- Resource: [Criterion benchmarking](https://github.com/bheisler/criterion.rs) +- Projet: Benchmark filters, optimize hot paths +- Validation: <100ms overhead on commands + +--- + +## 📊 Métriques de Succès + +### Phase 1 (Semaines 1-2) +- ✅ Vitest testé sur Aristote (90% token reduction) +- ✅ PR #6 mergée OU fork documenté comme stable +- ✅ 5+ projets adoptent RTK (dont 2 externes) + +### Phase 2 (Semaines 3-4) +- ✅ Blog post publié (500+ vues) +- ✅ 10+ GitHub stars +- ✅ 1+ contribution externe (issue/PR) + +### Phase 3 (Semaines 5-8) +- ✅ Tracing intégré (structured logs) +- ✅ Async refactor prototypé (si applicable) +- ✅ Config file support shipped + +### Phase 4 (Semaines 9-12) +- ✅ Homebrew formula published +- ✅ 50+ GitHub stars +- ✅ Utilisé en production par 3+ companies + +--- + +## 🚨 Risques & Mitigations + +### Risque 1: Maintainer upstream inactif +**Impact**: PRs jamais mergées +**Probabilité**: MEDIUM (1 semaine sans réponse) +**Mitigation**: Fork maintenu indépendamment, doc claire + +### Risque 2: Vitest breaking changes +**Impact**: Module obsolète +**Probabilité**: LOW (API stable) +**Mitigation**: Tests version-pinned, monitor releases + +### Risque 3: Async refactor trop coûteux +**Impact**: 3 semaines perdues sans ROI +**Probabilité**: MEDIUM +**Mitigation**: Prototyper d'abord, valider use case avant commit + +### Risque 4: Adoption faible +**Impact**: Effort gaspillé +**Probabilité**: LOW (besoin réel validé) +**Mitigation**: Focus Méthode Aristote d'abord, élargir après + +### Risque 5: Concurrence (autre tool similaire) +**Impact**: RTK devient obsolète +**Probabilité**: VERY LOW +**Mitigation**: Niche T3 Stack, first-mover advantage + +--- + +## 🎯 Décisions Stratégiques Immédiates + +### Décision 1: Upstream vs Fork Indépendant +**Deadline**: Fin Semaine 2 +**Critère**: Réponse maintainer sur PR #6 +**Action**: Si pas de réponse → Pivot vers fork + +### Décision 2: Async Refactor Go/No-Go +**Deadline**: Fin Phase 2 +**Critère**: Use cases LLM integration validés +**Action**: Si pas de demand → Skip Phase 3.1.1 + +### Décision 3: VSCode Extension Go/No-Go +**Deadline**: Fin Phase 3 +**Critère**: 50+ GitHub stars + 10+ actifs users +**Action**: Si pas atteint → Focus CLI uniquement + +--- + +## 🗓️ Timeline Visuelle + +``` +Semaines 1-2: ███████████████████████ Phase 1 (Production Ready) +├─ Vitest testing +├─ Issue #4 fix +├─ Documentation +└─ Cross-platform tests + +Semaines 3-4: ███████████████████████ Phase 2 (Upstream Engagement) +├─ PR monitoring +├─ Blog post +└─ Community building + +Semaines 5-8: ███████████████████████ Phase 3 (Advanced Features) +├─ Tracing integration +├─ Async prototype (conditional) +└─ Config file support + +Semaines 9-12: ██████████████████████ Phase 4 (Ecosystem & Scale) +├─ Homebrew tap +├─ Binary releases +└─ IDE integrations (conditional) +``` + +--- + +## 📞 Points de Contrôle + +**Weekly Reviews** (chaque lundi): +- Progrès vs roadmap +- Blockers identification +- Pivot decisions + +**Monthly Retrospectives**: +- Métriques adoption +- User feedback synthesis +- Roadmap adjustments + +**Stakeholders**: +- Florian (lead dev) +- Claude Code (AI pair programmer) +- Méthode Aristote team (beta users) +- Open source community (feedback loop) + +--- + +## 🎬 Prochaine Action Immédiate + +**TODAY** (2h): +1. ✅ Test Vitest sur Aristote +2. ✅ Measure token savings +3. ✅ Update README avec metrics + +**THIS WEEK** (5h): +1. Fix Issue #4 (grep/ls bugs) +2. Cross-platform test (Linux via Docker) +3. Create PR #7 (Vitest) si PR #6 stale + +**NEXT 2 WEEKS**: +- Decision point: Upstream vs Fork +- Community engagement (blog post) +- Onboard 2-3 external projects + +--- + +**Dernière mise à jour**: 2026-01-28 +**Auteur**: Florian Bruniaux +**Status**: ACTIVE - Phase 1 en cours diff --git a/src/git.rs b/src/git.rs index 7763edaf0..89274ca7f 100644 --- a/src/git.rs +++ b/src/git.rs @@ -26,6 +26,34 @@ pub fn run(cmd: GitCommand, args: &[String], max_lines: Option, verbose: } fn run_diff(args: &[String], max_lines: Option, verbose: u8) -> Result<()> { + // Check if user wants stat output + let wants_stat = args.iter().any(|arg| arg == "--stat" || arg == "--numstat" || arg == "--shortstat"); + + // Check if user wants compact diff (default RTK behavior) + let wants_compact = !args.iter().any(|arg| arg == "--no-compact"); + + if wants_stat || !wants_compact { + // User wants stat or explicitly no compacting - pass through directly + let mut cmd = Command::new("git"); + cmd.arg("diff"); + for arg in args { + cmd.arg(arg); + } + + let output = cmd.output().context("Failed to run git diff")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("{}", stderr); + std::process::exit(output.status.code().unwrap_or(1)); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + println!("{}", stdout.trim()); + return Ok(()); + } + + // Default RTK behavior: stat first, then compacted diff let mut cmd = Command::new("git"); cmd.arg("diff").arg("--stat"); @@ -132,31 +160,56 @@ fn compact_diff(diff: &str, max_lines: usize) -> String { result.join("\n") } -fn run_log(args: &[String], max_lines: Option, verbose: u8) -> Result<()> { - let limit = max_lines.unwrap_or(10); - +fn run_log(args: &[String], _max_lines: Option, verbose: u8) -> Result<()> { let mut cmd = Command::new("git"); - cmd.args([ - "log", - &format!("-{}", limit), - "--pretty=format:%h %s (%ar) <%an>", - "--no-merges", - ]); + cmd.arg("log"); + + // Check if user provided format flags + let has_format_flag = args.iter().any(|arg| { + arg.starts_with("--oneline") + || arg.starts_with("--pretty") + || arg.starts_with("--format") + }); + + // Check if user provided limit flag + let has_limit_flag = args.iter().any(|arg| arg.starts_with('-') && arg.chars().nth(1).map_or(false, |c| c.is_ascii_digit())); + // Apply RTK defaults only if user didn't specify them + if !has_format_flag { + cmd.args(["--pretty=format:%h %s (%ar) <%an>"]); + } + + if !has_limit_flag { + cmd.arg("-10"); + } + + // Only add --no-merges if user didn't explicitly request merge commits + let wants_merges = args.iter().any(|arg| arg == "--merges" || arg == "--min-parents=2"); + if !wants_merges { + cmd.arg("--no-merges"); + } + + // Pass all user arguments for arg in args { cmd.arg(arg); } let output = cmd.output().context("Failed to run git log")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + eprintln!("{}", stderr); + // Propagate git's exit code + std::process::exit(output.status.code().unwrap_or(1)); + } + let stdout = String::from_utf8_lossy(&output.stdout); if verbose > 0 { - eprintln!("Last {} commits:", limit); + eprintln!("Git log output:"); } - for line in stdout.lines().take(limit) { - println!("{}", line); - } + println!("{}", stdout.trim()); Ok(()) } diff --git a/src/main.rs b/src/main.rs index 8e8db4e0a..c61dfcbd5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ mod read; mod runner; mod summary; mod tracking; +mod vitest_cmd; mod wget_cmd; use anyhow::Result; @@ -232,25 +233,27 @@ enum Commands { #[arg(long)] create: bool, }, + + /// Vitest commands with compact output + Vitest { + #[command(subcommand)] + command: VitestCommands, + }, } #[derive(Subcommand)] enum GitCommands { /// Condensed diff output Diff { - #[arg(trailing_var_arg = true)] + /// Git arguments (supports all git diff flags like --stat, --cached, etc) + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec, - /// Max lines - #[arg(short, long)] - max_lines: Option, }, /// One-line commit history Log { - #[arg(trailing_var_arg = true)] + /// Git arguments (supports all git log flags like --oneline, --graph, --all) + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] args: Vec, - /// Number of commits - #[arg(short = 'n', long, default_value = "10")] - count: usize, }, /// Compact status Status, @@ -310,6 +313,16 @@ enum KubectlCommands { }, } +#[derive(Subcommand)] +enum VitestCommands { + /// Run tests with filtered output (90% token reduction) + Run { + /// Additional vitest arguments + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + args: Vec, + }, +} + fn main() -> Result<()> { let cli = Cli::parse(); @@ -327,11 +340,11 @@ fn main() -> Result<()> { } Commands::Git { command } => match command { - GitCommands::Diff { args, max_lines } => { - git::run(git::GitCommand::Diff, &args, max_lines, cli.verbose)?; + GitCommands::Diff { args } => { + git::run(git::GitCommand::Diff, &args, None, cli.verbose)?; } - GitCommands::Log { args, count } => { - git::run(git::GitCommand::Log, &args, Some(count), cli.verbose)?; + GitCommands::Log { args } => { + git::run(git::GitCommand::Log, &args, None, cli.verbose)?; } GitCommands::Status => { git::run(git::GitCommand::Status, &[], None, cli.verbose)?; @@ -472,6 +485,12 @@ fn main() -> Result<()> { config::show_config()?; } } + + Commands::Vitest { command } => match command { + VitestCommands::Run { args } => { + vitest_cmd::run(vitest_cmd::VitestCommand::Run, &args, cli.verbose)?; + } + }, } Ok(()) diff --git a/src/vitest_cmd.rs b/src/vitest_cmd.rs new file mode 100644 index 000000000..f08f08007 --- /dev/null +++ b/src/vitest_cmd.rs @@ -0,0 +1,296 @@ +use anyhow::{Context, Result}; +use regex::Regex; +use std::process::Command; +use crate::tracking; + +#[derive(Debug, Clone)] +pub enum VitestCommand { + Run, +} + +pub fn run(cmd: VitestCommand, args: &[String], verbose: u8) -> Result<()> { + match cmd { + VitestCommand::Run => run_vitest(args, verbose), + } +} + +fn run_vitest(args: &[String], verbose: u8) -> Result<()> { + let mut cmd = Command::new("pnpm"); + cmd.arg("vitest"); + cmd.arg("run"); // Force non-watch mode + + for arg in args { + cmd.arg(arg); + } + + let output = cmd.output().context("Failed to run vitest")?; + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + // Vitest returns non-zero exit code when tests fail + // This is expected behavior for test runners + let combined = format!("{}{}", stdout, stderr); + let filtered = filter_vitest_output(&combined); + + if verbose > 0 { + eprintln!("vitest run (filtered):"); + } + + println!("{}", filtered); + + tracking::track( + "vitest run", + "rtk vitest run", + &combined, + &filtered, + ); + + // Propagate original exit code + std::process::exit(output.status.code().unwrap_or(1)) +} + +/// Strip ANSI escape sequences from terminal output +fn strip_ansi(text: &str) -> String { + // Match ANSI escape sequences: \x1b[...m + let ansi_regex = Regex::new(r"\x1b\[[0-9;]*m").unwrap(); + ansi_regex.replace_all(text, "").to_string() +} + +/// Extract test statistics from Vitest output +#[derive(Debug, Default)] +struct TestStats { + pass: usize, + fail: usize, + total: usize, + duration: String, +} + +fn parse_test_stats(output: &str) -> TestStats { + let mut stats = TestStats::default(); + + // Strip ANSI first for easier parsing + let clean_output = strip_ansi(output); + + // Pattern: "Test Files X failed | Y passed | Z skipped (T)" + // Or: "Test Files Y passed (T)" when no failures + if let Some(caps) = Regex::new(r"Test Files\s+(?:(\d+)\s+failed\s+\|\s+)?(\d+)\s+passed").unwrap().captures(&clean_output) { + if let Some(fail_str) = caps.get(1) { + stats.fail = fail_str.as_str().parse().unwrap_or(0); + } + if let Some(pass_str) = caps.get(2) { + stats.pass = pass_str.as_str().parse().unwrap_or(0); + } + } + + // Pattern: "Tests X failed | Y passed (T)" + // Capture total passed count from Tests line + if let Some(caps) = Regex::new(r"Tests\s+(?:\d+\s+failed\s+\|\s+)?(\d+)\s+passed").unwrap().captures(&clean_output) { + if let Some(total_str) = caps.get(1) { + stats.total = total_str.as_str().parse().unwrap_or(0); + } + } + + // Pattern: "Duration 3.05s" (with optional details in parens) + if let Some(caps) = Regex::new(r"Duration\s+([\d.]+[ms]+)").unwrap().captures(&clean_output) { + if let Some(duration_str) = caps.get(1) { + stats.duration = duration_str.as_str().to_string(); + } + } + + stats +} + +/// Extract failure details from Vitest output +fn extract_failures(output: &str) -> Vec { + let mut failures = Vec::new(); + let clean_output = strip_ansi(output); + + // Look for FAIL markers and extract test names + error messages + let lines: Vec<&str> = clean_output.lines().collect(); + let mut in_failure = false; + let mut current_failure = String::new(); + + for line in lines { + // Start of failure block: "✗ test_name" + if line.contains('✗') || line.contains("FAIL") { + if !current_failure.is_empty() { + failures.push(current_failure.trim().to_string()); + } + current_failure = line.to_string(); + in_failure = true; + continue; + } + + // Collect error details (indented lines after ✗) + if in_failure { + if line.trim().is_empty() || line.starts_with(" Test Files") || line.starts_with(" Tests") { + in_failure = false; + if !current_failure.is_empty() { + failures.push(current_failure.trim().to_string()); + current_failure.clear(); + } + } else if line.starts_with(" ") { + current_failure.push('\n'); + current_failure.push_str(line.trim()); + } + } + } + + // Push last failure if exists + if !current_failure.is_empty() { + failures.push(current_failure.trim().to_string()); + } + + failures +} + +/// Filter Vitest output - show summary + failures only +fn filter_vitest_output(output: &str) -> String { + let stats = parse_test_stats(output); + let failures = extract_failures(output); + + let mut result = Vec::new(); + + // Summary line + if stats.total > 0 { + result.push(format!("PASS ({}) FAIL ({})", stats.pass, stats.fail)); + } + + // Failure details + if !failures.is_empty() { + result.push(String::new()); // Blank line + for (idx, failure) in failures.iter().enumerate() { + result.push(format!("{}. {}", idx + 1, failure)); + } + } + + // Timing + if !stats.duration.is_empty() { + result.push(String::new()); + result.push(format!("Time: {}", stats.duration)); + } + + // If parsing failed, return cleaned output (fallback) + if result.len() <= 1 { + return strip_ansi(output) + .lines() + .filter(|line| { + // Keep only meaningful lines + let trimmed = line.trim(); + !trimmed.is_empty() + && !trimmed.starts_with("│") + && !trimmed.starts_with("├") + && !trimmed.starts_with("└") + }) + .collect::>() + .join("\n"); + } + + result.join("\n") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_strip_ansi() { + let input = "\x1b[32m✓\x1b[0m test passed"; + let output = strip_ansi(input); + assert_eq!(output, "✓ test passed"); + assert!(!output.contains("\x1b")); + } + + #[test] + fn test_parse_test_stats_success() { + let output = r#" + ✓ src/auth.test.ts (5) + ✓ src/utils.test.ts (8) + + Test Files 2 passed (2) + Tests 13 passed (13) + Duration 450ms +"#; + let stats = parse_test_stats(output); + assert_eq!(stats.pass, 2); + assert_eq!(stats.fail, 0); + assert_eq!(stats.total, 13); + assert_eq!(stats.duration, "450ms"); + } + + #[test] + fn test_parse_test_stats_failures() { + let output = r#" + ✓ src/auth.test.ts (5) + ✗ src/utils.test.ts (8) 2 failed + + Test Files 1 failed | 1 passed (2) + Tests 2 failed | 11 passed (13) + Duration 520ms +"#; + let stats = parse_test_stats(output); + assert_eq!(stats.pass, 1); + assert_eq!(stats.fail, 1); + assert_eq!(stats.total, 11); // Only passed count in this pattern + } + + #[test] + fn test_extract_failures() { + let output = r#" + ✗ test_edge_case + AssertionError: expected 10 to equal 5 + at src/lib.rs:42 + + ✗ test_overflow + Panic: overflow at src/utils.rs:18 +"#; + let failures = extract_failures(output); + assert_eq!(failures.len(), 2); + assert!(failures[0].contains("test_edge_case")); + assert!(failures[0].contains("AssertionError")); + assert!(failures[1].contains("test_overflow")); + assert!(failures[1].contains("Panic")); + } + + #[test] + fn test_filter_vitest_output_all_pass() { + let output = r#" + ✓ src/auth.test.ts (5) + ✓ src/utils.test.ts (8) + + Test Files 2 passed (2) + Tests 13 passed (13) + Duration 450ms +"#; + let result = filter_vitest_output(output); + assert!(result.contains("PASS (2) FAIL (0)")); + assert!(result.contains("Time: 450ms")); + assert!(!result.contains("✓")); // Stripped + } + + #[test] + fn test_filter_vitest_output_with_failures() { + let output = r#" + ✓ src/auth.test.ts (5) + ✗ src/utils.test.ts (8) + ✗ test_parse_invalid + Expected: valid | Received: invalid + + Test Files 1 failed | 1 passed (2) + Tests 1 failed | 12 passed (13) + Duration 520ms +"#; + let result = filter_vitest_output(output); + assert!(result.contains("PASS (1) FAIL (1)")); + assert!(result.contains("test_parse_invalid")); + assert!(result.contains("Time: 520ms")); + } + + #[test] + fn test_filter_ansi_colors() { + let output = "\x1b[32m✓\x1b[0m \x1b[1mTests passed\x1b[22m\nTest Files 1 passed (1)\n Duration 100ms"; + let result = filter_vitest_output(output); + assert!(!result.contains("\x1b[")); + assert!(result.contains("PASS (1) FAIL (0)")); + } +}