diff --git a/Cargo.lock b/Cargo.lock index 665500b..74becac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "agent-manager" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index e5588ea..46d3fa2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "agent-manager" -version = "0.2.1" +version = "0.2.2" edition = "2021" description = "Manage local AI coding agents: list, install, start, stop, update — with automatic dependency handling and a YAML-driven catalog." license = "MIT" diff --git a/README.md b/README.md index 78f794e..de9cd2a 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,37 @@ -# agent-manager (am) +# 🚀 agent-manager — vos agents IA, gérés comme des apps -Gestionnaire en ligne de commande, écrit en **Rust**, pour les **agents IA de -coding installés localement** : lister, installer, démarrer, arrêter, -redémarrer, mettre à jour et désinstaller — avec gestion automatique des -dépendances (Node.js, Python, Go, Rust, Bun, etc.) et un catalogue YAML de -~70 agents prêts à l'emploi. +**am** liste, installe, démarre, met à jour et désinstalle vos agents IA de +coding (Claude Code, Codex, Aider, jcode, Prime Agent… **72 agents connus**) +en une ligne de commande — avec la gestion des dépendances (Node.js, Python, +Go, Rust…) prise en charge pour vous. + +> ✅ Rust · ✅ Windows, Linux, macOS · ✅ aucune dépendance runtime --- -## Sommaire +## ✨ Pourquoi am ? -1. [Fonctionnalités](#fonctionnalités) -2. [Installation](#installation) -3. [Démarrage rapide](#démarrage-rapide) -4. [Commandes](#commandes) -5. [Options globales](#options-globales) -6. [Fichier de configuration](#fichier-de-configuration) -7. [Ajouter un agent](#ajouter-un-agent) -8. [Alias, groupes et mode interactif](#alias-groupes-et-mode-interactif) -9. [Gestion des processus](#gestion-des-processus) -10. [Sécurité](#sécurité) -11. [Tests](#tests) -12. [Complétions shell](#complétions-shell) -13. [Structure du projet](#structure-du-projet) -14. [Limites et feuille de route](#limites-et-feuille-de-route) +- 🗂️ **Catalogue de 72 agents prêts à l'emploi** — descriptions, méthodes + d'installation, dépendances et commandes de démarrage déjà configurées. +- 📦 **9 méthodes d'installation** — npm, pip, uv, cargo, go, bun, script + d'installation, binaire (GitHub Releases/Gitea) et dépôt git — toujours + dans un répertoire local, jamais dans le système. +- 🧩 **Dépendances gérées pour vous** — avant d'installer un agent, am + vérifie les outils requis et, s'il en manque, **détecte votre OS** et + propose la commande exacte (scoop/winget, apt/dnf/pacman, brew) — il peut + même l'exécuter à votre place. +- 🚦 **Gestion des processus** — démarrage en avant-plan ou détaché + (--background) avec PID enregistré et logs ; arrêt propre SIGTERM puis + SIGKILL. +- ⚡ **Rapide** — sondes de version en parallèle + cache : un am list + à chaud prend ~25 ms. +- 🧭 **Shell interactif** avec complétion **Tab** et historique. +- 🔒 **Sûr** — mode --dry-run universel, checksums sha256, confirmations + avant les scripts d'installation, journalisation horodatée. --- -## Fonctionnalités - -- **Catalogue intégré** : ~70 agents connus (Claude Code, Codex, Gemini CLI, - OpenCode, Aider, jcode, Prime Agent, l'écosystème OpenClaw, les SaaS, etc.), - chacun avec sa méthode d'installation et ses dépendances. -- **9 méthodes d'installation** : - - | type | description | - |------|-------------| - | npm / bun | paquet global installé dans un préfixe local (jamais en -g système) | - | pip / uv | virtualenv privé par agent, sans toucher au site-packages système | - | cargo | cargo install --root | - | go | go install avec GOBIN local | - | curl | script d'installation téléchargé puis exécuté séparément (jamais de curl pipé vers sh) | - | binary| binaire précompilé depuis GitHub Releases ou URL directe, checksum vérifié, extraction zip/tar.gz | - | git | clone + build (make, cargo build --release, npm install…) puis copie du binaire ou shim | - -- **Gestion des processus** : avant-plan (stdio hérités) ou arrière-plan - (détaché, PID enregistré, sortie vers un log) ; arrêt propre SIGTERM puis - SIGKILL après un délai configurable. -- **Base d'état locale** (state.json) : nom, version, chemin, binaires, PID, - dates — plus la **détection d'agents externes** via le PATH. Les sondes de - version sont exécutées **en parallèle** (timeout 4 s) et **mises en cache** - (index du PATH et versions) : un am list à chaud prend ~50 ms. -- **Dépendances** : vérification avant installation (node >= 18, etc.) avec - messages actionnables ; installation automatique possible quand une commande - d'installation est connue. -- **Sécurité** : mode --dry-run universel, confirmations (--yes pour les - sauter), validation des URL (http/https/file), vérification sha256, - journalisation horodatée de toutes les commandes exécutées. -- **Confort développeur** : sortie JSON stable (--json), couleurs - (--no-color), mode verbeux, complétions bash/zsh/fish/powershell/elvish, - alias, groupes (am start group:dev), mode interactif, export/import, - doctor et self-update. - ---- - -## Installation - -### En une ligne (recommandé) +## ⚡ Installation — 30 secondes **Windows** (PowerShell) : @@ -76,358 +41,240 @@ dépendances (Node.js, Python, Go, Rust, Bun, etc.) et un catalogue YAML de curl -fsSL https://git.dracodev.net/Projets/agent-manager/raw/branch/main/install.sh | sh -Le script télécharge le binaire précompilé de la **dernière release** du dépôt -Gitea, l'installe dans un répertoire utilisateur et l'ajoute au PATH. Si aucun -binaire ne correspond à votre plateforme, il compile automatiquement depuis -les sources (cargo install --git). **Relancez la même commande pour mettre à -jour.** Les scripts sont versionnés dans le dépôt (install.ps1, install.sh) — -vous pouvez les lire avant de les exécuter. +Le script télécharge le binaire précompilé de la dernière release +(statique sous Linux : compatible toutes les distributions, Debian 12 et +antérieures comprises) et l'ajoute à votre PATH. Sans binaire pour votre +plateforme, il compile automatiquement depuis les sources. -### Depuis les sources - -Prérequis : Rust 1.70+ (le programme lui-même n'a **aucune** dépendance -runtime ; les outils comme node/python ne sont nécessaires que pour les agents -qui les utilisent). - - # 1. cloner et compiler (binaire dans target/release/am) - git clone https://git.dracodev.net/Projets/agent-manager.git - cd agent-manager - cargo build --release - - # 2. installer dans ~/.cargo/bin (sur le PATH) - cargo install --path . - - # 3. vérifier - am --version - am doctor - -> Le binaire s'appelle **am** (agent-manager est le nom du paquet). - -### Ajouter les binaires des agents au PATH - -Les agents installés par am vivent dans -~/.local/share/agent-manager/agents/bin (Linux), -%LOCALAPPDATA%\agent-manager\agents\bin (Windows) ou -~/Library/Application Support/agent-manager/agents/bin (macOS). -Ajoutez ce répertoire au PATH pour pouvoir lancer les agents directement : - - # Linux/macOS — dans ~/.bashrc ou ~/.zshrc - export PATH="$HOME/.local/share/agent-manager/agents/bin:$PATH" - -am doctor vous le rappelle si le répertoire n'est pas sur le PATH. +> 🔄 **Mise à jour** : relancez simplement la même commande. --- -## Démarrage rapide +## 🎬 Vos 3 premières commandes - am list # agents installés (gérés + externes) - am list --all # tout le catalogue, avec l'état de chacun - am search rust # recherche par mot-clé - am info jcode # fiche détaillée + am list # vos agents installés (et ceux détectés sur le PATH) - am install claude-code # npm + vérification de node >= 18 - am install aider --method pip # choisir la méthode 2 (pip) au lieu de uv - am install smelt --dry-run # simuler sans rien changer + NAME VERSION STATUS SOURCE PATH + ------------------------------------------------ + Claude Code 2.1.112 external external C:\\Users\\bruno\\.local\\bin\\claude.exe + jcode 0.76.0 external external C:\\Users\\bruno\\AppData\\Local\\jcode\\bin\\jcode.exe - am start claude-code # avant-plan (Ctrl-C pour quitter) - am start claude-code --background --env ANTHROPIC_MODEL=opus - am status claude-code - am stop claude-code # SIGTERM puis SIGKILL après 5 s - am restart codex --background - am update claude-code - am uninstall smelt --purge + am install jcode # installe un agent (dépendances vérifiées d'abord) - am run pi "explique ce fichier" # exécution directe, sans gestion de process - am doctor --fix # vérifie l'environnement et répare + installing jcode (type=binary repo=1jehuang/jcode)… + installed jcode (v0.76.0) in ~/.local/share/agent-manager/agents/jcode + + am start jcode # le lance en avant-plan (--background pour détacher) --- -## Commandes +## 📚 Aide-mémoire des commandes -| Commande | Description | -|----------|-------------| -| am list [--all] [--category ] | agents installés (défaut) ; --all pour tout le catalogue | -| am start [--background] [--args ...] [--env K=V] [--notify] | démarre l'agent (avant-plan par défaut) | -| am stop [--force] [--timeout ] | arrête un agent d'arrière-plan (PID enregistré) | -| am restart [--background] [--force] | arrête puis redémarre | -| am status [agent] | état d'un agent, ou de tous les agents installés | -| am install [--method ] [--force] | installe l'agent et ses dépendances | -| am uninstall [--purge] | désinstalle et nettoie (--purge : logs + entrée de config) | -| am update [--all] | met à jour vers la dernière version connue | -| am search [--category ] | recherche dans le catalogue | -| am info | description, méthodes d'installation, commande, dépendances, site | -| am config show / path / edit / validate / add | gestion de la configuration | -| am doctor [--fix] | outils présents, validité de la config, chemins, permissions | -| am run [args...] | exécute la commande de l'agent directement | -| am completion | script de complétion (bash, zsh, fish, powershell, elvish) | -| am self-update [--check] | met à jour agent-manager depuis GitHub Releases | -| am export / am import | sauvegarde/restauration de la config + état | -| am (sans arguments) | shell interactif | +### 🔍 Découvrir -**Statuts affichés par list/status** : running (PID actif), installed (géré -par am), external (trouvé sur le PATH, non géré), not-installed, -not-installable (SaaS/desktop). +| Commande | Rôle | +|----------|------| +| am list | agents installés (gérés + détectés sur le PATH) | +| am list --all | tout le catalogue, avec l'état de chacun | +| am search | recherche par nom, description, catégorie, tag | +| am info | fiche détaillée (installation, dépendances, site…) | +| am status [agent] | état, version, PID, logs | + +**Statuts** : 🟢 running · 🔵 installed (géré par am) · 🟡 external (trouvé +sur le PATH) · ⚪ not-installed · 🔴 not-installable (SaaS/desktop) + +### 📦 Installer et gérer + +| Commande | Rôle | +|----------|------| +| am install | installe (dépendances vérifiées et proposées) | +| am install --method pip | choisit une méthode d'installation | +| am update | met à jour vers la dernière version connue | +| am uninstall | désinstalle et nettoie (--purge : logs + config) | +| am export / am import | sauvegarde et restaure config + état | + +### 🚀 Exécuter + +| Commande | Rôle | +|----------|------| +| am start | avant-plan (Ctrl-C pour quitter) | +| am start --background --env KEY=VALEUR | détaché, PID et log gérés | +| am stop | arrêt gracieux puis forcé (--force : immédiat) | +| am restart | arrête puis redémarre | +| am run [args…] | exécution directe, sans gestion de processus | + +### 🧰 Système + +| Commande | Rôle | +|----------|------| +| am doctor | vérifie outils, config, chemins — avec commandes d'installation | +| am doctor --fix | répare ce qui peut l'être, propose d'installer les outils manquants | +| am config show / edit / add | gère votre configuration | +| am completion | script de complétion (bash, zsh, fish, powershell) | +| am self-update | met à jour am lui-même (si configuré) | +| am | shell interactif avec Tab et historique | --- -## Options globales +## 🧩 Dépendances : am s'en occupe -Toutes les options globales fonctionnent avant **ou après** la sous-commande. +Avant chaque installation, am vérifie les **prérequis** déclarés pour +l'agent (ex. node >= 18 pour Claude Code). S'il manque un outil : -| Option | Description | -|--------|-------------| -| -c, --config | fichier de configuration alternatif | -| -v, --verbose | affiche chaque commande exécutée et les détails | -| -q, --quiet | uniquement les erreurs | +1. il **détecte votre système** — Windows (scoop puis winget), Linux + (apt / dnf / pacman / apk selon la distribution), macOS (brew) ; +2. il **propose la commande exacte** et peut l'exécuter à votre place + (confirmation demandée, ou --yes) : + + $ am install claude-code + dependency 'node' is missing + Run the detected installer for Windows? -> scoop install nodejs-lts [y/N] + +Si vous refusez, l'installation s'arrête avec le mode d'emploi : + + error: agent 'claude-code' has unmet dependencies: + - node: not found on PATH (minimum: 18.0.0) + on Windows: scoop install nodejs-lts | winget install OpenJS.NodeJS.LTS + +am doctor affiche la même aide pour tous les outils de l'environnement. +Les dépendances des paquets (les modules npm/pip de l'agent lui-même) sont, +elles, installées automatiquement par npm/pip/cargo dans un **répertoire +local isolé** (virtualenv privé, --prefix npm, GOBIN local…) — votre +système n'est jamais touché. + +--- + +## 🛠️ Options globales + +Disponibles avant **ou après** la sous-commande. + +| Option | Effet | +|--------|-------| | -y, --yes | répond oui à toutes les confirmations | -| --dry-run | simule l'action (aucune écriture, aucun téléchargement) | -| --json | sortie JSON stable sur stdout (les messages vont sur stderr) | -| --no-color | désactive les couleurs ANSI | -| -h, --help / -V, --version | aide / version | +| --dry-run | simule sans rien modifier (idéal pour essayer) | +| --json | sortie JSON stable pour vos scripts | +| -v, --verbose | affiche chaque commande exécutée | +| -q, --quiet | uniquement les erreurs | +| --no-color | désactive les couleurs | +| -c, --config | configuration alternative | --- -## Fichier de configuration +## 👥 Groupes, alias et shell interactif -### Emplacements (par ordre de priorité) + am start cc # alias (cc -> claude-code) + am start group:dev # démarre tout un groupe en arrière-plan + am stop group:dev -1. --config (ou la variable d'environnement AGENT_MANAGER_CONFIG) + am # shell interactif : + am> inst # complétion Tab : commandes, agents, groupes, options + am> install jcode + am> exit # historique persistant (flèches haut/bas) + +--- + +## ⚙️ Personnalisation + +### Fichiers de configuration (par priorité) + +1. --config 2. ./agent-manager.yaml (répertoire courant) 3. ~/.config/agent-manager/config.yaml - (Windows : %APPDATA%\agent-manager\config.yaml) -4. catalogue par défaut embarqué dans le binaire +4. catalogue embarqué (72 agents) -Le fichier trouvé **surcharge/complète** le catalogue embarqué : un agent -défini par l'utilisateur remplace celui du même nom ; les autres s'ajoutent. +Vos définitions **complètent ou surchargent** le catalogue par nom. -### Schéma (extrait) - - version: "1.0" - - settings: - install_dir: null # défaut: ~/.local/share/agent-manager/agents - log_dir: null # défaut: ~/.local/state/agent-manager/logs - default_shell: null # shell pour les scripts d'installation - auto_install_deps: true # proposer d'installer les dépendances manquantes - confirm_before_run: true # confirmer avant d'exécuter un script d'installation - stop_timeout_secs: 5 # délai SIGTERM -> SIGKILL - self_update_repo: null # "owner/repo" pour am self-update - - aliases: - cc: claude-code # am start cc == am start claude-code - - groups: - dev: [claude-code, aider, codex] - - include: # modularité : autres fichiers (chemins relatifs) - - agents-perso.yaml - - agents: - - name: claude-code - display_name: "Claude Code" - description: "..." - category: coding-agent - website: https://github.com/anthropics/claude-code - install: - type: npm - package: "@anthropic-ai/claude-code" - # args: ["--omit=optional"] # arguments supplémentaires - # env: { FOO: bar } # environnement de l'installeur - # methods: [...] # méthodes alternatives (--method) - # post_install: ["claude setup"] # commandes après installation - dependencies: - - { name: node, min_version: "18.0.0", install_hint: "https://nodejs.org" } - run: claude - args: [] # arguments par défaut - env: {} # variables d'environnement par défaut - tags: [anthropic] - installable: true # false = listé mais non installable (SaaS/desktop) - note: null # explication affichée avec installable: false - platforms: [] # linux | macos | windows (vide = tous) - -Les chemins acceptent ~ et les variables d'environnement ($HOME, ${VAR}). -La configuration est **validée au démarrage** avec des messages précis -(agent 'x': 'run' is required when installable, champs inconnus rejetés, -doublons détectés). - ---- - -## Ajouter un agent - -Créez ~/.config/agent-manager/config.yaml (am config edit) ou un fichier -inclus (am config add mes-agents.yaml) puis : +### Ajouter votre agent + # ~/.config/agent-manager/config.yaml (ou am config add mon-fichier.yaml) version: "1.0" agents: - # paquet npm - - name: mon-agent-npm + - name: mon-agent display_name: "Mon Agent" install: { type: npm, package: mon-agent } dependencies: - { name: node, min_version: "18.0.0", install_hint: "https://nodejs.org" } run: mon-agent - # paquet pip (virtualenv privé) - - name: mon-agent-python - display_name: "Mon Agent Python" - install: { type: pip, package: mon-agent-python } - dependencies: - - { name: python, min_version: "3.10.0", install_hint: "https://python.org" } - run: mon-agent - - # binaire GitHub Releases (asset choisi selon plateforme/arch) - - name: mon-binaire - display_name: "Mon Binaire" - install: - type: binary - repo: owner/repo - binary: mon-binaire # nom de l'exécutable attendu - checksum: null # ou "sha256:..." - run: mon-binaire - - # binaire via URL directe - - name: mon-binaire-2 - display_name: "Mon Binaire 2" - install: - type: binary - url: "https://example.com/dl/mon-bin.tar.gz" - run: mon-bin - - # script d'installation (téléchargé PUIS exécuté, jamais curl|sh) - - name: mon-script - display_name: "Mon Script" - install: - type: curl - script: "https://example.com/install.sh" - run: mon-script - platforms: [linux, macos] - - # dépôt git + build - - name: mon-git - display_name: "Mon Git" - install: - type: git - repo: "https://github.com/owner/repo" - build: ["cargo", "build", "--release"] # ou "make build" (chaîne) - binary_path: target/release/mon-git # copié dans le bin dir - run: mon-git - - # application desktop / SaaS : listée mais non installable - - name: mon-saas - display_name: "Mon SaaS" - installable: false - note: "SaaS — voir https://example.com" - -Puis : am config validate, am install mon-agent-npm, etc. +Autres méthodes : pip/uv (virtualenv privé), cargo, go, bun, binaire depuis +Gitea/GitHub Releases (repo: owner/repo), URL directe, script d'installation +(type: curl) et dépôt git avec build (build + binary_path). Les chemins +acceptent ~ et les variables d'environnement ($HOME, ${VAR}) ; includes +multi-fichiers pris en charge ; configuration validée au démarrage. --- -## Alias, groupes et mode interactif +## 🔒 Sécurité - am start cc # alias -> claude-code - am start group:dev # démarre tout le groupe "dev" en arrière-plan - am stop group:dev # arrête tout le groupe - am # shell interactif (rustyline) : - # am> inst # complétion Tab : commandes, agents, alias, - # am> install jcode # group:xxx, options (--background, ...) - # am> start jcode --background - # am> exit - -Le shell interactif offre la **complétion par Tab** (commandes, noms -d'agents, alias, groupes, options) et un **historique persistant** -(flèches haut/bas, fichier history.txt dans le répertoire d'état). +- les scripts d'installation sont **téléchargés puis exécutés séparément** — + jamais de curl pipé vers sh ; --verbose les affiche ; +- **--dry-run** simule n'importe quelle action sans rien écrire ; +- checksums **sha256** vérifiés quand ils sont fournis ; +- URL limitées à https/http/file ; extraction d'archives protégée contre le + path traversal ; +- tout vit dans vos répertoires utilisateur ; rien n'est exécuté avec des + privilèges élevés. --- -## Gestion des processus +## 🖥️ Complétions shell -- **Avant-plan** (défaut) : la commande hérite de votre terminal ; am attend - sa fin et propage son code de sortie. Ctrl-C interrompt l'agent. -- **Arrière-plan** (--background) : processus détaché (nouveau groupe de - processus), PID enregistré dans state.json, stdout/stderr redirigés vers - ~/.local/state/agent-manager/logs/.log. Si l'agent crashe au - démarrage, am affiche la fin du log et échoue. -- **Arrêt** : am stop envoie SIGTERM (Windows : taskkill), attend - stop_timeout_secs, puis SIGKILL (taskkill /F /T). --force court-circuite - la phase gracieuse. Les PID périmés sont nettoyés automatiquement. -- Toutes les commandes exécutées sont journalisées avec horodatage dans - /agent-manager.log (--verbose les affiche aussi). - ---- - -## Sécurité - -- Les scripts d'installation (curl) sont **téléchargés puis exécutés - séparément** — jamais de curl pipé directement vers sh. --verbose affiche - leur contenu, confirm_before_run demande confirmation. -- --dry-run simule n'importe quelle action sans rien écrire. -- URL limitées à https, http et file (tests locaux). -- Checksums sha256 vérifiés quand ils sont fournis dans la config. -- Rien n'est exécuté avec des privilèges élevés ; tout vit dans les - répertoires utilisateur. Les archives sont extraites avec protection - contre le path traversal (zip-slip). - ---- - -## Tests - - cargo test # tests unitaires + intégration (config, versions, - # dry-run, processus, doctor, installateurs) - cargo test -- --nocapture - -Les tests d'installation utilisent des *mocks* et le mode dry-run : aucun -réseau, aucune installation réelle. Les tests de processus spawnent -sleep/ping et vérifient le cycle démarrage → détection → arrêt. - ---- - -## Complétions shell - - am completion bash > completions/am.bash # fournies dans completions/ + am completion bash > completions/am.bash am completion zsh > completions/_am am completion fish > completions/am.fish am completion powershell > completions/_am.ps1 - # bash - source completions/am.bash - # zsh - fpath+=(completions) && compinit - # fish - cp completions/am.fish ~/.config/fish/completions/am.fish +--- + +## ❓ FAQ + +**Où sont installés les agents ?** +~/.local/share/agent-manager/agents (Linux), %LOCALAPPDATA%\\agent-manager\\agents +(Windows) — l'état et les logs dans ~/.local/state/agent-manager (Linux) ou +%LOCALAPPDATA%\\agent-manager (Windows). + +**Comment mettre à jour am ?** Relancez l'installeur one-liner (ou +am self-update si le dépôt de release est configuré). + +**Comment désinstaller am ?** Supprimez le binaire et le répertoire +d'état ; les agents installés restent utilisables en les supprimant avec +am uninstall avant. + +**Pourquoi le binaire Linux est statique ?** Pour fonctionner partout, +Debian 12 (glibc 2.36) comprise, sans dépendance système. + +**Un agent me demande une clé API.** am ne gère pas les secrets : il lance +l'agent, qui fera sa propre configuration. Passez des variables avec +--env KEY=VALEUR si besoin. --- -## Structure du projet +## 🧑‍💻 Pour les développeurs + +### Compiler et tester + + git clone https://git.dracodev.net/Projets/agent-manager.git + cd agent-manager + cargo build --release # binaire dans target/release/am + cargo test # 45+ tests (config, processus, dry-run, complétion…) + +### Architecture src/ - main.rs # point d'entrée (binaire am) - lib.rs # crate bibliothèque (testable) - cli.rs # définition clap des commandes - app.rs # contexte applicatif (config, chemins, état, log) - config.rs # schéma YAML, chargement, includes, fusion, validation - catalog.rs # catalogue fusionné : lookup, alias, groupes, recherche - state.rs # base locale JSON (installations, PID) - runner.rs # exécution de commandes (réelle / mock / dry-run) - process.rs # spawn détaché, détection, SIGTERM/SIGKILL, logs - download.rs # téléchargement, checksums, releases GitHub, archives - deps.rs # vérification des dépendances (node >= 18, ...) - doctor.rs # diagnostic d'environnement + réparations - output.rs # logger horodaté, tableaux, JSON, couleurs - installers/ # npm, bun, pip, uv, cargo, go, curl, binary, git - commands/ # une implémentation par commande - repl.rs # mode interactif - config.yaml # catalogue par défaut (~70 agents, embarqué) - tests/ # tests d'intégration - completions/ # scripts de complétion générés + cli.rs # commandes clap + config.rs # schéma YAML, includes, fusion, validation + catalog.rs # catalogue : lookup, alias, groupes, recherche + deps.rs # vérification des dépendances (min_version, OS) + toolchain.rs # détection OS + commandes d'installation (scoop/apt/…) + probe.rs # détection d'agents externes (parallèle + cache) + installers/ # npm, pip, uv, cargo, go, bun, curl, binary, git + process.rs # spawn détaché, signaux, logs + state.rs # base locale JSON (installations, PID) + runner.rs # exécution de commandes (réelle, mock, dry-run) ---- +### Publier une release -## Limites et feuille de route - -- Les applications desktop (Warp, openyak, Kun, cc-haha) et les SaaS (Devin, - Amp, Junie, Cortex, Tabnine, Mentat) sont **listés** avec - installable: false et un lien — ils ne sont pas installables localement. -- La mise à jour compare la version installée à la dernière connue (registry - npm/PyPI/crates.io, tag GitHub) puis réinstalle. -- Pistes futures : plugins d'installation dynamiques, notifications Windows - natives, intégration tmux, tests d'intégration avec des registres simulés. +Les binaires des releases sont construits en conteneur : Windows (MSVC) +nativement, **Linux en statique musl** (jamais de binaire glibc récent — +voir l'erreur GLIBC). La procédure complète est dans +[RELEASING.md](RELEASING.md). diff --git a/src/commands/doctor_cmd.rs b/src/commands/doctor_cmd.rs index 35d6afd..42c68df 100644 --- a/src/commands/doctor_cmd.rs +++ b/src/commands/doctor_cmd.rs @@ -12,6 +12,10 @@ pub fn run(app: &App, fix: bool) -> Result { for r in &repaired { app.log.success(r); } + let runner = SystemRunner::new(false, app.cli.verbose, &app.log); + for r in doctor::install_missing_tools(app, &runner)? { + app.log.success(r); + } app.log.info("run 'am doctor' again to verify"); return Ok(0); } diff --git a/src/deps.rs b/src/deps.rs index 0398c19..a5f8ed8 100644 --- a/src/deps.rs +++ b/src/deps.rs @@ -111,34 +111,60 @@ pub fn check_agent(agent: &AgentDef, runner: &dyn Runner) -> Vec { } /// Fail with an actionable message when dependencies are missing. -/// With auto_install_deps enabled, dependencies that define an install_cmd -/// are offered for automatic installation first. +/// +/// With auto_install_deps enabled (the default), missing tools are offered +/// for installation right away: an explicit install_cmd from the config +/// first, otherwise the command detected for the current OS +/// (scoop/winget on Windows, apt/dnf/pacman/apk on Linux, brew on macOS). pub fn ensure_deps(app: &App, agent: &AgentDef, runner: &dyn Runner) -> Result<()> { let mut checks = check_agent(agent, runner); if checks.iter().all(|c| c.ok) { return Ok(()); } + let os = crate::toolchain::OsInfo::detect(); if app.config.settings.auto_install_deps() && !app.dry_run() { for dep in &agent.dependencies { - if dep.install_cmd.is_empty() { - continue; - } if check_dep(runner, dep).ok { continue; } - let cmd_line = dep.install_cmd.join(" "); app.log.warn(&format!("dependency '{}' is missing", dep.name)); - let proceed = app - .confirm(&format!("Run '{cmd_line}' to install it?")) - .unwrap_or(false); - if proceed { - let prog = dep.install_cmd[0].clone(); - let args: Vec = dep.install_cmd[1..].to_vec(); - let empty = BTreeMap::new(); - let out = runner.run(&prog, &args, &empty, None, false)?; - if out.code != 0 { - app.log - .warn(&format!("automatic installation of '{}' failed", dep.name)); + // 1) Explicit install command from the configuration. + if !dep.install_cmd.is_empty() { + let cmd_line = dep.install_cmd.join(" "); + if app.confirm(&format!("Run '{cmd_line}' to install it?"))? { + let prog = dep.install_cmd[0].clone(); + let args: Vec = dep.install_cmd[1..].to_vec(); + let empty = BTreeMap::new(); + let out = runner.run(&prog, &args, &empty, None, false)?; + if out.code != 0 { + app.log.warn(&format!( + "automatic installation of '{}' failed (exit code {})", + dep.name, out.code + )); + } + } + continue; + } + // 2) Command detected for the current OS. + let cmds = crate::toolchain::install_commands(&dep.name, &os); + if let Some(cmd) = cmds.first() { + let proceed = app + .confirm(&format!("Run the detected installer for {}? -> {cmd}", os.name))?; + if proceed { + if let Ok(tokens) = shell_words::split(cmd) { + if !tokens.is_empty() { + let prog = tokens[0].clone(); + let args: Vec = tokens[1..].to_vec(); + let empty = BTreeMap::new(); + let out = runner.run(&prog, &args, &empty, None, false)?; + if out.code != 0 { + app.log.warn(&format!( + "installation of '{}' failed (exit code {})", + dep.name, out.code + )); + } + } + } } } } @@ -153,7 +179,15 @@ pub fn ensure_deps(app: &App, agent: &AgentDef, runner: &dyn Runner) -> Result<( if let Some(min) = &c.min { line.push_str(&format!(" (minimum: {min})")); } - if let Some(dep) = agent.dependencies.iter().find(|d| d.name == c.name) { + let cmds = crate::toolchain::install_commands(&c.name, &os); + if !cmds.is_empty() { + line.push_str(&format!( + " + on {}: {}", + os.name, + cmds.join(" | ") + )); + } else if let Some(dep) = agent.dependencies.iter().find(|d| d.name == c.name) { if let Some(hint) = &dep.install_hint { line.push_str(&format!(" — install via {hint}")); } diff --git a/src/doctor.rs b/src/doctor.rs index 9ab66bc..473a7e2 100644 --- a/src/doctor.rs +++ b/src/doctor.rs @@ -63,6 +63,7 @@ pub fn run(app: &App, runner: &dyn Runner) -> DoctorReport { ("uv", false), ("docker", false), ]; + let os = crate::toolchain::OsInfo::detect(); for (tool, required) in tools { match runner.which(tool) { Some(path) => { @@ -84,14 +85,20 @@ pub fn run(app: &App, runner: &dyn Runner) -> DoctorReport { }); } None => { - let (status, detail) = if *required { - ( - Status::Error, - "required but not found on PATH".to_string(), - ) + let mut detail = if *required { + "required but not found on PATH".to_string() } else { - (Status::Ok, "not installed".to_string()) + "not installed".to_string() }; + let cmds = crate::toolchain::install_commands(tool, &os); + if !cmds.is_empty() { + detail.push_str(&format!( + " — on {}: {}", + os.name, + cmds.join(" | ") + )); + } + let status = if *required { Status::Error } else { Status::Ok }; checks.push(Check { name: tool.to_string(), status, @@ -199,6 +206,42 @@ fn is_on_path(dir: &std::path::Path) -> bool { }) } +/// Offer to install the missing optional tools (node, python, ...) with +/// the command detected for the current OS. Returns what was repaired. +pub fn install_missing_tools(app: &App, runner: &dyn Runner) -> Result> { + let mut installed = Vec::new(); + let os = crate::toolchain::OsInfo::detect(); + for tool in ["node", "python", "cargo", "go", "bun", "uv", "docker"] { + if runner.which(tool).is_some() { + continue; + } + let cmds = crate::toolchain::install_commands(tool, &os); + let Some(cmd) = cmds.first() else { + continue; + }; + if !app.confirm(&format!( + "'{tool}' is missing — run the detected installer for {}? -> {cmd}", + os.name + ))? { + continue; + } + if let Ok(tokens) = shell_words::split(cmd) { + if !tokens.is_empty() { + let prog = tokens[0].clone(); + let args: Vec = tokens[1..].to_vec(); + let empty = BTreeMap::new(); + let out = runner.run(&prog, &args, &empty, None, false)?; + installed.push(if out.code == 0 { + format!("installed '{tool}' ({cmd})") + } else { + format!("failed to install '{tool}' (exit code {})", out.code) + }); + } + } + } + Ok(installed) +} + /// Repair what can be repaired: create directories, fix a corrupt state file. pub fn fix(app: &App) -> Result> { let mut repaired = Vec::new(); diff --git a/src/installers/mod.rs b/src/installers/mod.rs index 39b609b..ab195ec 100644 --- a/src/installers/mod.rs +++ b/src/installers/mod.rs @@ -136,11 +136,24 @@ pub fn sh_inherit( Ok(()) } -/// Require a tool on the PATH, with an actionable error message. +/// Require a tool on the PATH, with an actionable error message that +/// includes the install command detected for the current OS. pub fn require_tool(runner: &dyn Runner, tool: &str, hint: &str) -> Result { - runner - .which(tool) - .ok_or_else(|| anyhow!("'{tool}' not found on PATH — install it first ({hint})")) + if let Some(p) = runner.which(tool) { + return Ok(p); + } + let os = crate::toolchain::OsInfo::detect(); + let mut msg = format!("'{tool}' not found on PATH — install it first ({hint})"); + let cmds = crate::toolchain::install_commands(tool, &os); + if !cmds.is_empty() { + msg.push_str(&format!( + " + on {}: {}", + os.name, + cmds.join(" | ") + )); + } + Err(anyhow!(msg)) } /// Detect a binary's version via --version, then -V. diff --git a/src/lib.rs b/src/lib.rs index 8d2c4a5..1bb7ec7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ pub mod process; pub mod repl; pub mod runner; pub mod state; +pub mod toolchain; pub mod version; use clap::Parser; diff --git a/src/toolchain.rs b/src/toolchain.rs new file mode 100644 index 0000000..db207c8 --- /dev/null +++ b/src/toolchain.rs @@ -0,0 +1,306 @@ +//! OS detection and concrete installation commands for missing toolchains. +//! +//! When an agent requires a tool (node, python, go, ...) that is missing, +//! agent-manager detects the operating system — and the package manager +//! already present (scoop/winget on Windows, apt/dnf/pacman/apk on Linux, +//! brew on macOS) — and proposes the exact command to install it. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Family { + Windows, + Macos, + Linux, +} + +#[derive(Debug, Clone)] +pub struct OsInfo { + pub name: String, + pub family: Family, + /// Package manager found on Linux: apt, dnf, pacman, apk or zypper. + pub linux_pkg: Option, + pub has_scoop: bool, + pub has_winget: bool, +} + +impl Default for OsInfo { + fn default() -> Self { + Self::detect() + } +} + +impl OsInfo { + /// Detect the current operating system and available package managers. + pub fn detect() -> Self { + #[cfg(windows)] + { + let has_scoop = which::which("scoop").is_ok(); + let has_winget = which::which("winget").is_ok(); + return Self { + name: "Windows".to_string(), + family: Family::Windows, + linux_pkg: None, + has_scoop, + has_winget, + }; + } + #[cfg(target_os = "macos")] + { + return Self { + name: "macOS".to_string(), + family: Family::Macos, + linux_pkg: None, + has_scoop: false, + has_winget: false, + }; + } + #[cfg(not(any(windows, target_os = "macos")))] + { + let id = linux_distro_id(); + let (name, pkg) = linux_pkg_for_id(&id); + return Self { + name: name.to_string(), + family: Family::Linux, + linux_pkg: pkg.map(|s| s.to_string()), + has_scoop: false, + has_winget: false, + }; + } + } +} + +#[cfg(not(any(windows, target_os = "macos")))] +fn linux_distro_id() -> String { + let Ok(text) = std::fs::read_to_string("/etc/os-release") else { + return String::new(); + }; + for line in text.lines() { + if let Some(v) = line.strip_prefix("ID=") { + return v.trim().trim_matches('"').to_string(); + } + } + String::new() +} + +/// Map a distro id (from /etc/os-release) to (display name, package manager). +pub fn linux_pkg_for_id(id: &str) -> (&'static str, Option<&'static str>) { + match id { + "debian" | "ubuntu" | "linuxmint" | "pop" | "raspbian" | "elementary" => { + ("Debian/Ubuntu", Some("apt")) + } + "fedora" | "rhel" | "centos" | "rocky" | "almalinux" | "ol" => { + ("Fedora/RHEL", Some("dnf")) + } + "arch" | "manjaro" | "endeavouros" | "artix" => ("Arch Linux", Some("pacman")), + "alpine" => ("Alpine", Some("apk")), + "opensuse-tumbleweed" | "opensuse-leap" | "suse" => ("openSUSE", Some("zypper")), + _ => ("Linux", None), + } +} + +/// Concrete install commands for a tool on the detected OS, best option +/// first (scoop before winget when both exist, etc.). +pub fn install_commands(tool: &str, os: &OsInfo) -> Vec { + let mut out = Vec::new(); + match os.family { + Family::Windows => { + if os.has_scoop { + if let Some(c) = scoop_command(tool) { + out.push(c); + } + } + if os.has_winget || out.is_empty() { + if let Some(c) = winget_command(tool) { + out.push(c); + } + } + } + Family::Macos => { + if let Some(c) = brew_command(tool) { + out.push(c); + } + } + Family::Linux => { + if let Some(pkg) = os.linux_pkg.as_deref() { + if let Some(c) = linux_command(tool, pkg) { + out.push(c); + } + } + } + } + out +} + +fn scoop_command(tool: &str) -> Option { + let bucket = match tool { + "git" => "git", + "node" | "npm" => "nodejs-lts", + "python" | "python3" | "pip" => "python", + "cargo" | "rust" => "rustup", + "go" => "go", + "bun" => "bun", + "uv" => "uv", + "docker" => "docker", + _ => return None, + }; + Some(format!("scoop install {bucket}")) +} + +fn winget_command(tool: &str) -> Option { + let package = match tool { + "git" => "Git.Git", + "node" | "npm" => "OpenJS.NodeJS.LTS", + "python" | "python3" | "pip" => "Python.Python.3.12", + "cargo" | "rust" => "Rustlang.Rustup", + "go" => "GoLang.Go", + "bun" => "Oven-sh.Bun", + "uv" => "astral-sh.uv", + "docker" => "Docker.DockerDesktop", + _ => return None, + }; + Some(format!( + "winget install --accept-source-agreements --accept-package-agreements {package}" + )) +} + +fn brew_command(tool: &str) -> Option { + Some( + match tool { + "git" => "brew install git".to_string(), + "curl" => "brew install curl".to_string(), + "node" | "npm" => "brew install node".to_string(), + "python" | "python3" | "pip" => "brew install python".to_string(), + "cargo" | "rust" => "brew install rustup".to_string(), + "go" => "brew install go".to_string(), + "bun" => "brew install oven-sh/bun/bun".to_string(), + "uv" => "brew install uv".to_string(), + "docker" => "brew install --cask docker".to_string(), + _ => return None, + }, + ) +} + +fn linux_command(tool: &str, pkg: &str) -> Option { + Some( + match (tool, pkg) { + ("git", "apt") => "sudo apt install -y git", + ("git", "dnf") => "sudo dnf install -y git", + ("git", "pacman") => "sudo pacman -S --noconfirm git", + ("git", "apk") => "sudo apk add git", + ("git", "zypper") => "sudo zypper install -y git", + ("curl", "apt") => "sudo apt install -y curl", + ("curl", "dnf") => "sudo dnf install -y curl", + ("curl", "pacman") => "sudo pacman -S --noconfirm curl", + ("curl", "apk") => "sudo apk add curl", + ("curl", "zypper") => "sudo zypper install -y curl", + ("node" | "npm", "apt") => "sudo apt install -y nodejs npm", + ("node" | "npm", "dnf") => "sudo dnf install -y nodejs npm", + ("node" | "npm", "pacman") => "sudo pacman -S --noconfirm nodejs npm", + ("node" | "npm", "apk") => "sudo apk add nodejs npm", + ("node" | "npm", "zypper") => "sudo zypper install -y nodejs npm", + ("python" | "python3", "apt") => "sudo apt install -y python3 python3-venv python3-pip", + ("python" | "python3", "dnf") => "sudo dnf install -y python3 python3-pip", + ("python" | "python3", "pacman") => "sudo pacman -S --noconfirm python python-pip", + ("python" | "python3", "apk") => "sudo apk add python3 py3-pip", + ("python" | "python3", "zypper") => "sudo zypper install -y python3 python3-pip", + ("pip", "apt") => "sudo apt install -y python3-pip", + ("pip", "dnf") => "sudo dnf install -y python3-pip", + ("pip", "pacman") => "sudo pacman -S --noconfirm python-pip", + ("pip", "apk") => "sudo apk add py3-pip", + ("pip", "zypper") => "sudo zypper install -y python3-pip", + ("cargo" | "rust", "apt") => { + "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh" + } + ("cargo" | "rust", "dnf") => "sudo dnf install -y rust cargo", + ("cargo" | "rust", "pacman") => "sudo pacman -S --noconfirm rust", + ("cargo" | "rust", "apk") => "sudo apk add rust cargo", + ("cargo" | "rust", "zypper") => "sudo zypper install -y rust cargo", + ("go", "apt") => "sudo apt install -y golang-go", + ("go", "dnf") => "sudo dnf install -y golang", + ("go", "pacman") => "sudo pacman -S --noconfirm go", + ("go", "apk") => "sudo apk add go", + ("go", "zypper") => "sudo zypper install -y go", + ("bun", "apt") => "curl -fsSL https://bun.sh/install | bash", + ("bun", "dnf") => "curl -fsSL https://bun.sh/install | bash", + ("bun", "pacman") => "curl -fsSL https://bun.sh/install | bash", + ("bun", "apk") => "sudo apk add bun", + ("bun", "zypper") => "curl -fsSL https://bun.sh/install | bash", + ("uv", "apt") => "curl -LsSf https://astral.sh/uv/install.sh | sh", + ("uv", "dnf") => "sudo dnf install -y uv", + ("uv", "pacman") => "sudo pacman -S --noconfirm uv", + ("uv", "apk") => "sudo apk add uv", + ("uv", "zypper") => "sudo zypper install -y uv", + ("docker", "apt") => "sudo apt install -y docker.io", + ("docker", "dnf") => "sudo dnf install -y docker", + ("docker", "pacman") => "sudo pacman -S --noconfirm docker", + ("docker", "apk") => "sudo apk add docker", + ("docker", "zypper") => "sudo zypper install -y docker", + _ => return None, + } + .to_string(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn distro_mapping() { + assert_eq!(linux_pkg_for_id("debian").1, Some("apt")); + assert_eq!(linux_pkg_for_id("ubuntu").1, Some("apt")); + assert_eq!(linux_pkg_for_id("fedora").1, Some("dnf")); + assert_eq!(linux_pkg_for_id("arch").1, Some("pacman")); + assert_eq!(linux_pkg_for_id("alpine").1, Some("apk")); + assert_eq!(linux_pkg_for_id("opensuse-tumbleweed").1, Some("zypper")); + assert_eq!(linux_pkg_for_id("").1, None); + } + + #[test] + fn windows_commands() { + assert_eq!(scoop_command("node").as_deref(), Some("scoop install nodejs-lts")); + assert_eq!(scoop_command("uv").as_deref(), Some("scoop install uv")); + assert!(winget_command("git").unwrap().contains("Git.Git")); + assert!(winget_command("python").unwrap().contains("--accept-source-agreements")); + assert!(scoop_command("unknown-tool").is_none()); + } + + #[test] + fn linux_commands() { + assert!(linux_command("node", "apt").unwrap().contains("nodejs")); + assert!(linux_command("cargo", "apt").unwrap().contains("rustup")); + assert!(linux_command("go", "pacman").unwrap().contains("pacman -S")); + assert!(linux_command("python", "apk").unwrap().contains("python3")); + } + + #[test] + fn macos_commands() { + assert_eq!(brew_command("node").as_deref(), Some("brew install node")); + assert!(brew_command("docker").unwrap().contains("--cask")); + } + + #[test] + fn os_aware_selection() { + // Windows without scoop falls back to winget. + let os = OsInfo { + name: "Windows".into(), + family: Family::Windows, + linux_pkg: None, + has_scoop: false, + has_winget: true, + }; + let cmds = install_commands("node", &os); + assert!(!cmds.is_empty()); + assert!(cmds[0].contains("winget")); + // Debian-like systems get apt commands. + let deb = OsInfo { + name: "Debian".into(), + family: Family::Linux, + linux_pkg: Some("apt".into()), + has_scoop: false, + has_winget: false, + }; + let cmds = install_commands("go", &deb); + assert!(!cmds.is_empty()); + assert!(cmds[0].contains("apt")); + } +}