Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 le PATH,
  • 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 :

  1. Résout le nom de binaire haw-<name> et le lance via une recherche dans le PATH.
  2. Transmet <args...> tels quels comme argv du plugin (haw ne les analyse pas).
  3. Transmet le contexte de l’espace de travail sous forme de document JSON haw.plugin/1 de 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.)
  4. Laisse les stdout et stderr du plugin connectés au terminal (ou au pipe avec lequel haw a été invoqué) — le plugin affiche directement.
  5. 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"] }
  ]
}
ChampSignification
schemaToujours "haw.plugin/1". Vérifiez-le avant de faire confiance au reste.
rootRacine absolue de l’espace de travail (l’arborescence contenant haw.toml).
stackNom 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=1 est définie, et
  • le JSON de contexte (sur HAW_JSON et 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 :

  1. Structurée — un document haw.plugin.view/1. haw affiche son title suivi de chaque chaîne dans lines :

    {
      "schema": "haw.plugin.view/1",
      "title": "SBOM status",
      "lines": [
        "kernel   ✓ SBOM emitted",
        "hal      ✓ SBOM emitted",
        "app-mqtt ⚠ stale"
      ]
    }
    
  2. Texte brut — tout ce qui n’est pas un document haw.plugin.view/1 est affiché tel quel comme corps du panneau. Cela permet à un plugin de printf un 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 le PATH.
  • 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 :

--langFichier(s) d’entréeLe rendre exécutable
shellhaw-<name> (POSIX sh exécutable)déjà exécutable — déposer sur le PATH
pythonhaw-<name> (exécutable, python3)déjà exécutable — déposer sur le PATH
gomain.go + go.mod (module haw-<name>)go build -o haw-<name>
rustCargo.toml ([[bin]] haw-<name>) + src/main.rscargo build --release

Chaque squelette est livré avec un README.md contenant la recette « déposer sur le PATHhaw <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"
    }
  ]
}
ChampSignification
schemaToujours "haw.plugins.index/1".
plugins[]Une entrée par plugin.
nameLe verbe que les utilisateurs saisissent (haw <name>).
crateNom du crate pour cargo install (facultatif).
gitURL du dépôt source (facultatif).
descriptionRé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 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 :

  • Pythonbindings/python (haw_plugin) : Context.from_env(), Report.emit(), view(title, lines). Aucune dépendance au-delà de la stdlib.
  • Gobindings/go (hawplugin) : ReadContext(), Report.Emit(), View(title, lines). Stdlib uniquement.
  • Shell POSIX et Rust — le examples/haw-hello et 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.

PluginLangageCe qu’il fait / comment l’essayer
haw-fleet-statusPOSIX shPanneau de santé compact par dépôt — branche, modifié ?, en avance/en retard. Pur git, zéro dépendance. haw fleet-status
haw-dockerPOSIX shSignale 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-webPython 3Compte/valide les *.html (doctype, titre, équilibre des balises), signale les *.css, rapporte les tailles. Bibliothèque standard uniquement. haw web
haw-k8sPOSIX shTrouve 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-aiPython 3Ré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écute haw sync, affiche le résultat.
  • :HawStatushaw status dans un buffer temporaire.
  • :HawDash — ouvre haw dash (le cockpit TUI) dans une division de terminal.
  • :HawFleet — liste la flotte (dépôt / branche / état) dans un buffer temporaire, analysé depuis haw 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-jirahaw jira. Gardez-le court et non revendiqué par les commandes intégrées (haw --help les liste).
  • --help auto-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 document haw.plugin/1 (ou votre propre schéma versionné) sous --format json afin 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 avec 0 en 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/repos existent. 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 json et le contexte haw.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 avec cargo install haw-<name>, qui le dépose dans ~/.cargo/bin (généralement sur le PATH).
  • Livrez un binaire ou un script. Déposez haw-<name> dans n’importe quel répertoire du PATH (/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.