Plugins
haw suit le modèle git / cargo / kubectl : toute sous-commande que haw ne reconnaît pas est déléguée à un exécutable présent sur votre PATH. Livrez haw-jira, haw-bazel, haw-sbom-scan sans toucher au cœur.
Un plugin est :
- un exécutable nommé
haw-<name>quelque part sur lePATH, - exécuté comme un processus séparé — un plugin défaillant ou bloqué ne peut pas faire planter haw,
- il reçoit le contexte de l’espace de travail au format JSON afin de pouvoir agir sur la flotte courante.
Contrat de répartition
Lorsque vous exécutez haw <name> <args...> et que <name> n’est pas une commande intégrée, haw :
- Résout le nom de binaire
haw-<name>et le lance via une recherche dans lePATH. - Transmet
<args...>tels quels comme argv du plugin (haw ne les analyse pas). - Transmet le contexte de l’espace de travail sous forme de document JSON
haw.plugin/1de deux façons :- dans la variable d’environnement
HAW_JSON, et - écrit sur le stdin du plugin. (Les deux transportent le document identique — lisez celui qui vous convient.)
- dans la variable d’environnement
- Laisse les stdout et stderr du plugin connectés au terminal (ou au pipe avec lequel haw a été invoqué) — le plugin affiche directement.
- Attend le plugin et propage son code de sortie comme code de sortie propre de haw (borné à 0–255 ; un plugin tué par un signal remonte comme
1).
Si aucune commande intégrée ne correspond et qu’aucun haw-<name> n’est trouvé sur le PATH, haw échoue avec :
error: no built-in `<name>` and no `haw-<name>` on PATH: No such file or directory (os error 2)
et se termine avec un code non nul. La répartition échoue proprement : un mot inconnu est une erreur, jamais un plantage.
Le contexte haw.plugin/1
Le document JSON que haw fournit (via HAW_JSON et stdin). À l’intérieur d’un espace de travail, il est entièrement renseigné :
{
"schema": "haw.plugin/1",
"root": "/path/to/workspace",
"stack": "gateway",
"repos": [
{ "name": "kernel", "path": "/path/to/workspace/kernel", "rev": "v6.1.2", "groups": ["firmware"] },
{ "name": "hal", "path": "/path/to/workspace/hal", "rev": "main", "groups": ["firmware"] }
]
}
| Champ | Signification |
|---|---|
schema | Toujours "haw.plugin/1". Vérifiez-le avant de faire confiance au reste. |
root | Racine absolue de l’espace de travail (l’arborescence contenant haw.toml). |
stack | Nom du stack courant, ou null si aucun n’est sélectionné. |
repos[] | Dépôts résolus : name, path absolu, rev et groups. |
Exécuté en dehors d’un espace de travail, haw répartit toujours le plugin mais le contexte se réduit au seul marqueur de schéma :
{ "schema": "haw.plugin/1" }
Un plugin bien conçu vérifie la présence de root/repos et adopte un comportement sensé lorsqu’ils sont absents (afficher l’aide, opérer sur le répertoire courant, ou se terminer proprement).
Panneaux TUI — afficher votre propre surface de cockpit
Le cockpit (haw dash) dispose d’une vue Plugins à part entière (appuyez sur 7, ou :plugins) qui liste chaque plugin disponible — les clés du [plugins] du manifeste unies avec les exécutables haw-* découverts sur le PATH. En sélectionner un avec Enter exécute le plugin dans une render intent et affiche sa sortie dans le panneau de détail défilable intitulé plugin: <name>.
Le contrat de rendu ajoute deux signaux au contexte haw.plugin/1 normal afin qu’un plugin puisse savoir qu’on lui demande un panneau lisible par un humain (plutôt que d’être déclenché pour une phase du cycle de vie) :
- la variable d’environnement
HAW_RENDER=1est définie, et - le JSON de contexte (sur
HAW_JSONet stdin) porte"intent": "render".
{
"schema": "haw.plugin/1",
"intent": "render",
"root": "/path/to/workspace",
"stack": "gateway",
"repos": [ /* ... comme ci-dessus ... */ ]
}
Lorsqu’il les détecte, le plugin doit afficher un panneau sur stdout et se terminer. Deux formes de sortie sont acceptées :
-
Structurée — un document
haw.plugin.view/1. haw affiche sontitlesuivi de chaque chaîne danslines:{ "schema": "haw.plugin.view/1", "title": "SBOM status", "lines": [ "kernel ✓ SBOM emitted", "hal ✓ SBOM emitted", "app-mqtt ⚠ stale" ] } -
Texte brut — tout ce qui n’est pas un document
haw.plugin.view/1est affiché tel quel comme corps du panneau. Cela permet à un plugin deprintfun rapport simple sans aucun JSON.
La sortie est limitée en nombre de lignes pour borner le panneau. Un plugin qui ne produit aucune sortie affiche un court texte de remplacement. Un plugin qui n’est pas sur le PATH signale une erreur claire dans le cockpit plutôt que de le faire planter.
Gérer les plugins
haw plugins (au pluriel) est la surface de gestion — découvrir ce qui existe, voir ce qui est installé, et installer les plugins de première partie. C’est une sous-commande définie, elle n’entre donc jamais en collision avec la répartition haw <name> : haw plugins list exécute toujours la commande intégrée, même si un binaire haw-plugins se trouve sur le PATH.
haw plugins list
Un tableau fusionnant trois sources, dédupliquées par nom :
- Catalogue officiel — les plugins de première partie livrés dans ce dépôt.
- Installés — chaque exécutable
haw-<name>trouvé sur lePATH. - Abonnés — les entrées
[plugins]du manifeste de l’espace de travail et leurs phases (lorsqu’exécuté à l’intérieur d’un espace de travail ; se réduit proprement lorsqu’il n’y en a aucun).
$ haw plugins list
NAME STATUS SUBSCRIBED DESCRIPTION
artifact available - SLSA/in-toto provenance + cosign/minisign signing
aspice installed pre-request ASPICE/qualification traceability from the pinned fleet
compliance available post-build SBOM (CycloneDX + SPDX) generation
...
STATUS vaut installed lorsque le binaire haw-<name> est sur le PATH, sinon available. SUBSCRIBED liste les phases du manifeste, ou -. Un plugin découvert sur le PATH qui ne figure pas dans le catalogue apparaît tout de même (avec la source path).
--format json émet un document haw.plugins/1 pour l’outillage :
haw plugins list --format json | jq '.plugins[] | select(.installed | not) | .name'
{
"schema": "haw.plugins/1",
"plugins": [
{
"name": "aspice",
"crate": "haw-aspice",
"installed": true,
"subscribed_phases": ["pre-request"],
"description": "Traçabilité ASPICE/qualification depuis la flotte épinglée",
"source": "catalog"
}
]
}
haw plugins install <name>
Installe un binaire de plugin via cargo install. Un nom de catalogue (aspice) est résolu vers son crate (haw-aspice) ; toute autre valeur est utilisée telle quelle, un nom de crate complet fonctionne donc aussi. Les plugins de première partie sont membres de l’espace de travail (pas encore sur crates.io), la source par défaut est donc --git https://github.com/Nastwinns/hawser :
haw plugins install aspice # cargo install --git <repo> haw-aspice
haw plugins install aspice --dry-run # afficher la commande, ne rien exécuter
haw plugins install haw-foo --git https://example.com/me/plugins # source personnalisée
haw plugins install haw-foo --git https://example.com/me/plugins --tag v1.2.0 # épingler à un tag
haw plugins install haw-foo --git https://example.com/me/plugins --rev 9f3c1a2 # épingler à un commit
haw plugins install some-crate --locked # respecter le Cargo.lock du crate
Épinglez une source --git personnalisée avec --tag <TAG> ou --rev <SHA> (mutuellement exclusifs). Ils ne s’appliquent qu’à une source --git personnalisée — la source de première partie par défaut est déjà épinglée automatiquement à cette version de hawser. Les installations sont toujours --locked pour la reproductibilité (--locked est désormais sans effet, conservé pour compatibilité).
haw affiche exactement ce qu’il va exécuter ($ cargo install …) avant de le lancer, diffuse la sortie de cargo et propage le code de sortie de cargo. --dry-run affiche la commande et se termine sans toucher à cargo. Si cargo n’est pas sur le PATH, haw échoue avec une erreur exploitable pointant vers https://rustup.rs.
haw plugins path
Affiche les répertoires que haw parcourt à la recherche de plugins haw-* (les entrées du PATH) — déposez un exécutable haw-<name> dans l’un d’entre eux pour le rendre détectable :
haw plugins path
Générer le squelette d’un plugin
haw plugins new <name> --lang <rust|python|go|shell> [--dir <path>] écrit un squelette de plugin exécutable qui implémente déjà le contrat : il lit le contexte haw.plugin/1 depuis HAW_JSON (en se rabattant sur stdin), gère --help et --format json, émet un document haw.plugin.report/1, et se réduit proprement lorsqu’il est exécuté en dehors d’un espace de travail. La cible par défaut est ./haw-<name> ; --dir la remplace. haw refuse d’écraser un répertoire non vide.
haw plugins new sbom --lang shell # ./haw-sbom/haw-sbom (POSIX sh) + README.md
haw plugins new sbom --lang python # ./haw-sbom/haw-sbom (python3) + README.md
haw plugins new sbom --lang go # ./haw-sbom/{main.go, go.mod, README.md}
haw plugins new sbom --lang rust # crate cargo : Cargo.toml + src/main.rs + README.md
haw plugins new sbom --lang shell --dir /tmp/sbom # choisir le répertoire cible
Selon le langage, le point d’entrée et l’étape de build diffèrent :
--lang | Fichier(s) d’entrée | Le rendre exécutable |
|---|---|---|
shell | haw-<name> (POSIX sh exécutable) | déjà exécutable — déposer sur le PATH |
python | haw-<name> (exécutable, python3) | déjà exécutable — déposer sur le PATH |
go | main.go + go.mod (module haw-<name>) | go build -o haw-<name> |
rust | Cargo.toml ([[bin]] haw-<name>) + src/main.rs | cargo build --release |
Chaque squelette est livré avec un README.md contenant la recette « déposer sur le PATH → haw <name> » et un extrait d’abonnement [plugins]. Les squelettes rust et go sont autonomes — celui en rust ne dépend que de serde/serde_json, d’aucun crate haw. Après le build, placez le binaire sur le PATH et exécutez-le :
haw plugins new demo --lang shell --dir /tmp/haw-demo
PATH="/tmp/haw-demo:$PATH" haw demo
HAW_JSON='{"schema":"haw.plugin/1"}' /tmp/haw-demo/haw-demo --format json
La sortie de haw plugins new liste chaque fichier créé et affiche les étapes suivantes exactes (build + invocation PATH=…) pour le langage choisi.
Découvrir les plugins communautaires
haw plugins list --remote fusionne un index communautaire dans le tableau local. Chaque plugin fusionné affiche le STATUS available et la source remote avec sa description ; tout ce qui est déjà installé, dans le catalogue ou abonné conserve son propre statut (la déduplication se fait par nom).
haw plugins list --remote
haw plugins list --remote --index https://example.com/plugins-index.json
haw plugins list --remote --format json # les entrées distantes portent "source":"remote"
L’URL d’index par défaut est https://raw.githubusercontent.com/Nastwinns/hawser/main/plugins-index.json ; passez --index <url> pour pointer vers le vôtre. Un échec réseau ou d’analyse n’est pas fatal — haw affiche un avertissement et se rabat sur la liste locale uniquement.
Le format haw.plugins.index/1
L’index est un unique document JSON :
{
"schema": "haw.plugins.index/1",
"plugins": [
{
"name": "sbom",
"crate": "haw-sbom",
"git": "https://github.com/you/haw-sbom",
"description": "Génération de SBOM CycloneDX pour la flotte épinglée"
}
]
}
| Champ | Signification |
|---|---|
schema | Toujours "haw.plugins.index/1". |
plugins[] | Une entrée par plugin. |
name | Le verbe que les utilisateurs saisissent (haw <name>). |
crate | Nom du crate pour cargo install (facultatif). |
git | URL du dépôt source (facultatif). |
description | Résumé d’une phrase affiché dans haw plugins list. |
Ajouter votre plugin à l’index communautaire
Ouvrez une PR sur le plugins-index.json à la racine du dépôt qui ajoute une entrée — name, crate, git, et une description d’une phrase. Une fois fusionnée, elle apparaît pour tous ceux qui exécutent haw plugins list --remote.
Interface machine — consommer la propre sortie de haw
Les plugins ont rarement besoin de re-dériver l’état de la flotte : les commandes de lecture de haw parlent déjà JSON. Chaque commande de lecture (status, tree, change status, verify, evidence) offre --format json avec un schéma stable et versionné et des codes de sortie stables. Faites appel à haw et analysez sa sortie :
haw status --format json | jq '.repos[] | select(.dirty)'
Le contexte haw.plugin/1 vous indique où se trouve l’espace de travail ; --format json vous indique dans quel état il se trouve. Voir EXTENDING.md §1.5 pour le contrat d’interface machine.
Hello, plugin — en deux langages
Les deux versions ci-dessous implémentent la même commande : haw hello affiche un message de salutation, --help se décrit lui-même, et --format json émet du JSON haw.plugin/1.
Shell POSIX
Une version complète et fonctionnelle se trouve dans examples/haw-hello. L’essentiel :
#!/usr/bin/env sh
set -eu
case "${1:-}" in
-h | --help)
echo "haw-hello — say hello. Options: --help, --format json"
exit 0
;;
esac
# haw nous transmet le contexte de l'espace de travail dans $HAW_JSON (et sur stdin).
root=$(printf '%s' "${HAW_JSON:-}" | sed -n 's/.*"root":"\([^"]*\)".*/\1/p')
if [ "${1:-}" = "--format" ] && [ "${2:-}" = "json" ]; then
printf '{"schema":"haw.plugin/1","plugin":"hello","root":"%s"}\n' "$root"
exit 0
fi
if [ -n "$root" ]; then
printf 'hello from haw-hello — workspace at %s\n' "$root"
else
printf 'hello from haw-hello (no workspace here)\n'
fi
Rendez-le exécutable et déposez-le sur le PATH :
chmod +x haw-hello
PATH="$PWD:$PATH" haw hello
Rust
Un binaire autonome — aucune dépendance envers un crate haw.
cargo new --bin haw-hello
cd haw-hello
src/main.rs :
use std::env;
use std::process::ExitCode;
fn main() -> ExitCode {
let args: Vec<String> = env::args().skip(1).collect();
if args.iter().any(|a| a == "-h" || a == "--help") {
println!("haw-hello — say hello. Options: --help, --format json");
return ExitCode::SUCCESS;
}
// haw transmet le contexte haw.plugin/1 dans HAW_JSON (également sur stdin).
let ctx = env::var("HAW_JSON").unwrap_or_default();
let root = ctx
.split("\"root\":\"")
.nth(1)
.and_then(|s| s.split('"').next())
.unwrap_or("");
if args == ["--format", "json"] {
println!(r#"{{"schema":"haw.plugin/1","plugin":"hello","root":"{root}"}}"#);
return ExitCode::SUCCESS;
}
if root.is_empty() {
println!("hello from haw-hello (no workspace here)");
} else {
println!("hello from haw-hello — workspace at {root}");
}
ExitCode::SUCCESS
}
Compilez-le et exécutez-le comme un plugin :
cargo build --release
PATH="$PWD/target/release:$PATH" haw hello
(Pour de vrais plugins, analysez HAW_JSON avec serde_json plutôt que par découpage de chaînes.)
Écrire dans n’importe quel langage
Le contrat de plugin est indépendant du langage — ce n’est que du JSON sur HAW_JSON / stdin (haw.plugin/1) et du JSON sur stdout (haw.plugin.report/1 pour les phases du cycle de vie, haw.plugin.view/1 pour la render intent du TUI). Tout langage capable de lire une variable d’environnement et d’afficher du JSON peut être un plugin haw.
Le répertoire schemas/ contient les JSON Schemas officiels (draft 2020-12) — la source de vérité pour chaque nom et forme de champ. Validez les E/S de votre plugin par rapport à eux.
De fines liaisons de référence reflètent ces schémas afin que vous n’ayez pas à écrire le JSON à la main :
- Python —
bindings/python(haw_plugin) :Context.from_env(),Report.emit(),view(title, lines). Aucune dépendance au-delà de la stdlib. - Go —
bindings/go(hawplugin) :ReadContext(),Report.Emit(),View(title, lines). Stdlib uniquement. - Shell POSIX et Rust — le
examples/haw-helloet la section « Hello, plugin » ci-dessus montrent des implémentations sans aucune dépendance.
Pour une liste organisée de plugins existants à installer ou dont vous inspirer, voir AWESOME-HAW-PLUGINS.md.
Plugins d’exemple de démarrage
Le répertoire examples/plugins/ fournit de petits starters exécutables — de zéro à quelques dépendances légères, POSIX sh ou bibliothèque standard Python 3, tous en lecture seule / dry-run par défaut. Chacun est un vrai exécutable haw-<name> avec --help, --format json (un haw.plugin.report/1) et l’intent de rendu du cockpit (un haw.plugin.view/1). Copies-en un sur ton PATH et lis-le comme modèle.
| Plugin | Langage | Ce qu’il fait / comment l’essayer |
|---|---|---|
haw-fleet-status | POSIX sh | Panneau de santé compact par dépôt — branche, modifié ?, en avance/en retard. Pur git, zéro dépendance. haw fleet-status |
haw-docker | POSIX sh | Signale les ressources Dockerfile/compose par dépôt ; lint avec hadolint et vérifie les images locales avec docker lorsqu’il est présent (dégrade en douceur). haw docker |
haw-web | Python 3 | Compte/valide les *.html (doctype, titre, équilibre des balises), signale les *.css, rapporte les tailles. Bibliothèque standard uniquement. haw web |
haw-k8s | POSIX sh | Trouve les *.yaml sous k8s//deploy//manifests/ et valide chacun avec kubectl apply --dry-run=client (hors ligne ; ne touche jamais un cluster). haw k8s |
haw-commit-ai | Python 3 | Rédige le texte de commit/PR à partir de ton changeset — et fait aussi office de serveur MCP pour que Claude lise les diffs. haw commit-ai |
Intégration éditeur — Neovim
examples/nvim (haw.nvim) est un petit plugin Lua sans dépendance qui exécute haw depuis Neovim en appelant le binaire haw via le shell — pas de serveur :
:HawSync— exécutehaw sync, affiche le résultat.:HawStatus—haw statusdans un buffer temporaire.:HawDash— ouvrehaw dash(le cockpit TUI) dans une division de terminal.:HawFleet— liste la flotte (dépôt / branche / état) dans un buffer temporaire, analysé depuishaw status --format json.
Installe avec lazy.nvim / packer / le :packadd natif — voir le README.
Conventions
- Nommez-le
haw-<verb>. Le verbe est ce que les utilisateurs saisissent :haw-jira→haw jira. Gardez-le court et non revendiqué par les commandes intégrées (haw --helples liste). --helpauto-descriptif. Les utilisateurs y découvrent les options de votre plugin ; haw ne documente pas les plugins à votre place.- Humain sur stdout, JSON sur
--format json. Affichez une ligne lisible par défaut ; émettez un documenthaw.plugin/1(ou votre propre schéma versionné) sous--format jsonafin que d’autres outils puissent vous chaîner via un pipe. - Les codes de sortie ont un sens.
0= succès. Non nul = échec, et haw le propage — les portes de CI et les chaînes&&en dépendent. Ne sortez pas avec0en cas d’erreur. - Échouez proprement. Gérez élégamment le contexte sans espace de travail (JSON limité au schéma). Ne présumez pas que
root/reposexistent. Ne vous bloquez jamais : votre processus bloque haw jusqu’à ce qu’il se termine. - Restez un processus séparé. Vous obtenez l’isolation gratuitement — n’essayez pas d’accéder aux entrailles de haw ; consommez plutôt
--format jsonet le contextehaw.plugin/1.
Distribuer votre plugin
Tout exécutable nommé haw-<name> sur le PATH fonctionne. Deux voies courantes :
- Publiez un crate. Nommez le binaire
haw-<name>; les utilisateurs l’obtiennent aveccargo install haw-<name>, qui le dépose dans~/.cargo/bin(généralement sur lePATH). - Livrez un binaire ou un script. Déposez
haw-<name>dans n’importe quel répertoire duPATH(/usr/local/bin,~/.local/bin,~/bin). Les scripts shell comptent — marquez-les comme exécutables.
Vérifiez avec :
which haw-<name> # haw finds exactly what your shell finds
haw <name> --help
Soumettre votre plugin
Vous avez construit quelque chose d’utile ? Partagez-le. Voir CONTRIBUTING.md pour la liste de vérification build/test et les règles de bienséance des PR, puis ouvrez une PR qui ajoute votre plugin à la liste communautaire — une ligne : nom, description d’une phrase, et un lien. Nous gardons le cœur petit à dessein ; l’écosystème vit dans les plugins.
Phases du cycle de vie
Les plugins peuvent s’abonner aux phases du cycle de vie dans la table [plugins] du manifeste et sont invoqués hors processus avec --haw-phase <name> (par ex. un plugin SBOM sur post-build). Le crate SDK facultatif haw-plugin offre aux auteurs Rust l’ergonomie Context/Report tout en compilant vers un binaire haw-<name> autonome.