SPB Git forge
3commits 1branches 0releases
2.9 MBsize
maindefault branch
1 h agolast push
HTML 57.2% Shell 19.1% Python 11.2% CSS 7.4% JavaScript 5.1%

remote-first : MACLUSTR-PROCEDURE.md (règle + propriétaires + mld adopt/garde), TRANSFERT-OPERATEUR.md + operateur/{export-connaissances,autoriser-operateur,installer-operateur}.sh, avertissement mld stage

simon-pierre boucher committed 2 h ago (Oct 2, 2026) parent 1710603

6 changed files +241 −0

added MACLUSTR-PROCEDURE.md +54 −0
@@ -0,0 +1,54 @@
1 +# Procédure standard MacLustr — REMOTE-FIRST (en vigueur depuis le 2026-10-02)
2 +
3 +> **Règle unique : tout ce qui touche MacLustr se modifie SUR LE NŒUD CONCERNÉ, jamais sur le laptop.**
4 +> Le laptop est un terminal : il ouvre des sessions SSH, lance les wrappers (`mld`, `mlt`, `mlmobile`) et consulte des miroirs
5 +> en lecture seule (`git pull`). La source de vérité de chaque composant est le **dépôt git sur son nœud propriétaire**, dont
6 +> l'`origin` est **spbgit** (bare repos `M1M32:~/srv/git/<nom>.git`, alias SSH `gitsrv` présent sur tous les nœuds et le laptop).
7 +> Généralisation à tout le cluster de la règle appliquée aux projets Groupe Ka depuis le 2026-08-17 (`KA-REMOTE-DEV.md`).
8 +
9 +## 1. Qui possède quoi
10 +
11 +| Composant | Nœud propriétaire | Dépôt sur le nœud | spbgit | Mise en service après modification |
12 +|---|---|---|---|---|
13 +| Orchestrateur **mld** (placement, heal, tunnel, harden) | **M1M32** | `~/maclustr-dispatch` | `maclustr-dispatch.git` | immédiat (`bin/mld`) ; `mld heal` (LaunchDaemon `io.maclustr.mld-heal`) relit le code à chaque passage |
14 +| **agentd** (API MacLustr pour apps macOS/iOS, sondes, incidents) | **M4M64a** | `~/maclustr-agentd` | `maclustr-agentd.git` | `launchctl kickstart -k gui/$(id -u)/io.maclustr.agentd` puis `curl :9210/api/summary` (version) |
15 +| **cluster-skill** (cette doc, NODES.md, inventaires, wrappers `mld`/`mlt`/`mlmobile`, `maclustr-tunnel/`, `ovh-manager/`, `dossier/`, `operateur/`) | **M1M32** | `~/cluster-skill` | `cluster-skill.git` | laptop : `git -C ~/Desktop/Cluster/cluster-skill pull` (les wrappers s'exécutent depuis ce miroir) |
16 +| **MacLustr Tunnel** (hub WireGuard, Caddy, `tunnelctl`) | **BHS64** (OVH) | `/usr/local/bin/tunnelctl`, `/etc/caddy`, `/etc/wireguard`, `/etc/maclustr-tunnel` | copie de référence `cluster-skill/maclustr-tunnel/` (à resynchroniser depuis BHS64 après modification) | `systemctl reload caddy` ; routes via `tunnelctl` |
17 +| **Manifestes et registre mld** (secrets) | **M1M32** | `~/dispatch/apps/*.json`, `~/dispatch/registry.json` | hors git (secrets) — sauvegardes `~/Desktop/Cluster/secrets/` | `mld deploy`/`mld status` |
18 +| **Apps déployées par mld** (Ka, UQO, MacLustr web, vquant, spb-cloud, qc26, pdb-api…) | nœud du registre (`mld status`) | **le dossier déployé lui-même** (`dir` du manifeste) est un clone git | `<app>.git` | build + `pm2 restart <procs>` sur le nœud, healthcheck local puis public |
19 +| **Apps Apple** (MacLustr macOS/iOS, Admin-Ka macOS/iOS, Ka iOS, Vrai-Prix iOS) | **exception : laptop** (Xcode, licence, signature, TestFlight) | `~/Desktop/Cluster/CLUSTER_APPLE/…`, `~/Desktop/Groupe-KA/…` | `*-ios.git`, `*-macos.git` | scripts `release-*.sh` ; **l'agent `agentd.py` n'en fait pas partie** (voir M4M64a) |
20 +| **Connaissances Claude Code** (CLAUDE.md global, mémoire, commandes `/cluster*`) | laptop opérateur | `~/.claude/…` | export dans `cluster-skill/operateur/connaissances/` (`operateur/export-connaissances.sh --vers-cluster-skill`) | voir `TRANSFERT-OPERATEUR.md` |
21 +
22 +Les copies laptop (`~/Desktop/Cluster/maclustr-dispatch`, `~/Desktop/Cluster/cluster-skill`, `~/Desktop/Projets/*`, `~/Desktop/Cluster/CLUSTER_APPLE/maclustr-ios-v2/agent/`) sont des **miroirs** : `git pull` pour lire, jamais d'édition ni de `git push` depuis le laptop (sauf apps Apple et export des connaissances).
23 +
24 +## 2. Modifier un composant : la séquence
25 +
26 +1. **Localiser** : `mld status` (app → nœud, dossier) ou le tableau ci-dessus.
27 +2. **Lire** : `ssh <nœud> "cd <dir> && git status --short && git log --oneline -5"`. Un `git status` non vide = travail en cours sur le nœud, à intégrer, pas à écraser.
28 +3. **Éditer sur le nœud** : petits changements par `ssh <nœud> 'python3 - <<EOF … EOF'` / `sed -i ''` ; changements substantiels : `scp <nœud>:<dir>/<fichier> /tmp/` → édition locale → `scp` retour. Les secrets restent dans les manifestes mld / `.env` du nœud (jamais dans git).
29 +4. **Construire et tester sur le nœud** (les `node_modules`, venv, bases y sont) : `pnpm build` / `pytest` / …, puis `pm2 restart <procs>` (ou `launchctl kickstart -k …` pour agentd/gardiens), healthcheck local (`curl 127.0.0.1:<port>`) puis public (`curl https://<domaine>`).
30 +5. **Committer et pousser sur le nœud** : `ssh <nœud> "cd <dir> && git add -A && git commit -m '…' && git push origin main"` (agent forwarding actif depuis le laptop).
31 +6. **Documenter** sur M1M32 si l'inventaire ou une procédure change : `ssh M1M32 "cd ~/cluster-skill && … && git commit && git push"` puis `git pull` sur le laptop ; mettre à jour `~/.claude/CLAUDE.md` et la mémoire Claude si la règle ou la topologie change.
32 +
33 +## 3. mld et la règle remote-first
34 +
35 +- **`mld adopt <app> [--node X] [--repo nom] [--dry-run]`** (2026-10-02) : fait du dossier déployé un clone git (`origin` spbgit, créé s'il n'existe pas), restaure depuis `origin/main` les fichiers suivis absents du nœud (exclus du rsync : tests, exemples) et **commite l'état du nœud** (`adopt: état déployé sur <nœud>…`). À faire une fois par app ; toutes les apps du registre sont adoptées au 2026-10-02.
36 +- **Garde dans `mld deploy`** : si le dossier du nœud est un dépôt git dont le HEAD diffère du staging, ou qui a des modifications non commitées, le déploiement **refuse** (le `rsync --delete` écraserait du travail). Chemin normal : `mld fetch <app> --from <nœud>` (copie le `.git` dans le staging) puis `mld deploy`, ou `mld move` qui le fait seul. `--force` écrase sciemment.
37 +- **`mld stage <dir-local> <app>`** ne sert plus qu'à **créer une app** la première fois (ou à la ressusciter depuis un dépôt) : stage → `mld deploy` → `mld adopt`. Jamais pour modifier une app existante.
38 +- Déplacer une app : `mld move <app> --to X` (pré-copie à chaud depuis le nœud, `.git` compris, route tunnel repointée). Nouveau nœud → `mld prepare X`.
39 +
40 +## 4. Vérifier que tout fonctionne (après toute opération)
41 +
42 +```bash
43 +mld status --live # apps : PM2 online, santé locale/publique
44 +mld heal --dry-run # ce que l'auto-réparation ferait (doit être « 0 réparées, 0 KO »)
45 +mld tunnel status # pairs WireGuard (16 Macs) + routes Caddy sur BHS64
46 +curl -s -H "Authorization: Bearer $(ssh M4M64a "python3 -c 'import json;print(json.load(open(\"$HOME/maclustr-agentd/config.json\"))[\"token\"])'")" http://M4M64a.maclustr.io:9210/api/summary | python3 -m json.tool | head -30 # agentd : health, servers, sites, incidents
47 +```
48 +
49 +## 5. Ce qui a été fait le 2026-10-02 pour aligner l'état sur la règle
50 +
51 +- `maclustr-agentd.git` créé ; `M4M64a:~/maclustr-agentd` est un dépôt (config/lanmap/registry/history hors git) — la copie `CLUSTER_APPLE/maclustr-ios-v2/agent/` du laptop devient miroir.
52 +- `cluster-skill.git` créé ; `M1M32:~/cluster-skill` = copie canonique, `~/Desktop/Cluster/cluster-skill` = miroir (wrappers).
53 +- `mld adopt` + garde `deploy` ; 18 apps adoptées (voir journal `M1M32:~/dispatch/logs/`), dont spb-cloud et vquant dont l'`origin` GitHub (compte suspendu) a été renommé `old-origin`.
54 +- Alias `gitsrv` de M1M32 → lui-même (127.0.0.1) ; les autres nœuds visent M1M32 par le LAN.
added TRANSFERT-OPERATEUR.md +56 −0
@@ -0,0 +1,56 @@
1 +# Transférer les connaissances Claude Code et l'accès MacLustr sur un autre ordinateur
2 +
3 +Un « opérateur » MacLustr = un Mac avec (1) l'accès réseau et SSH au cluster, (2) l'outillage `cluster-skill`, (3) les connaissances
4 +Claude Code (instructions globales, mémoire, commandes). Les trois scripts de `operateur/` automatisent le transfert.
5 +
6 +## Ce qui constitue la connaissance Claude Code sur cet ordinateur
7 +
8 +| Élément | Emplacement | Rôle |
9 +|---|---|---|
10 +| Instructions globales | `~/.claude/CLAUDE.md` | inventaire, règles, procédure remote-first, tunnel, mld… chargé à chaque session |
11 +| Mémoire persistante | `~/.claude/projects/-Users-<user>/memory/*.md` + `MEMORY.md` | faits appris au fil des sessions (dossier **lié au chemin du projet** : `-Users-simon-pierreboucher` = sessions lancées depuis `$HOME`) |
12 +| Commandes | `~/.claude/commands/cluster*.md` | `/cluster`, `/cluster-status`, `/cluster-run`, `/cluster-compute`, `/cluster-deploy` |
13 +| Réglages | `~/.claude/settings.json` | modèle, hooks, plugins |
14 +| Serveurs MCP | `~/.claude.json` → `mcpServers` | **contiennent des jetons** (Apify, Tavily, Notion…) — export seulement avec `--mcp` |
15 +| Accès | `~/.ssh/config` (alias des 29 Macs + BHS64 + gitsrv), `~/.ssh/maclustr-egress.sh`, clé ed25519 | la clé privée **ne se copie pas** : on en génère une sur le nouveau Mac et on l'autorise |
16 +| Outillage | `~/Desktop/Cluster/cluster-skill` (lien `~/Desktop/cluster-skill`) | clone de `gitsrv:srv/git/cluster-skill.git` : wrappers `mld`, `mlt`, `mlmobile`, docs |
17 +
18 +## Prérequis réseau sur le nouveau Mac
19 +
20 +1. **Tailscale** installé et connecté au **même tailnet** : les noms `*.maclustr.io` (GoDaddy) pointent sur les IP Tailscale `100.x` des nœuds. Si l'ACL du tailnet restreint les sources (le SSH inter-nœuds y est bloqué), autoriser la nouvelle machine dans la console Tailscale.
21 +2. Homebrew + `python3`, `rsync`, `jq` (installés par le script si absents). `pandoc`/`weasyprint` seulement pour régénérer les PDF.
22 +3. Même nom d'utilisateur macOS (`simon-pierreboucher`) recommandé : les chemins absolus des documents restent valables. Sinon le script réécrit `/Users/simon-pierreboucher` → nouveau `$HOME` dans CLAUDE.md, la mémoire et la config SSH.
23 +
24 +## Procédure
25 +
26 +**Sur l'ordinateur actuel (source)**
27 +```bash
28 +~/Desktop/cluster-skill/operateur/export-connaissances.sh [--mcp] [--vers-cluster-skill]
29 +# → ~/Desktop/maclustr-operateur-<date>.tar.gz (0600)
30 +# --mcp inclut les serveurs MCP et leurs jetons
31 +# --vers-cluster-skill copie aussi CLAUDE.md + mémoire + commandes dans cluster-skill/operateur/connaissances/ et pousse sur spbgit
32 +# (seule édition de cluster-skill autorisée depuis le laptop : c'est un export)
33 +```
34 +Transférer l'archive (AirDrop, clé USB, `scp` vers un nœud puis vers le nouveau Mac).
35 +
36 +**Sur le nouvel ordinateur**
37 +```bash
38 +# 1. clé + config SSH + Claude (sans réseau cluster) — depuis l'archive
39 +tar -xzf maclustr-operateur-<date>.tar.gz -C /tmp/op && bash /tmp/op/operateur/installer-operateur.sh maclustr-operateur-<date>.tar.gz
40 +# génère ~/.ssh/id_ed25519 si absent et affiche la clé publique à autoriser
41 +```
42 +**Sur l'ordinateur actuel** (ou n'importe quelle machine déjà autorisée, M1M32 compris) :
43 +```bash
44 +~/Desktop/cluster-skill/operateur/autoriser-operateur.sh "<clé publique affichée>" # 29 Macs + BHS64 (ubuntu) + gitsrv
45 +```
46 +**Sur le nouvel ordinateur**, relancer l'installeur : il clone `cluster-skill`, pose le lien `~/Desktop/cluster-skill`, vérifie `ssh M1M32` et `mld status`.
47 +```bash
48 +bash /tmp/op/operateur/installer-operateur.sh maclustr-operateur-<date>.tar.gz
49 +```
50 +Puis ouvrir Claude Code depuis `$HOME` (`cd ~ && claude`) : CLAUDE.md et la mémoire sont chargés.
51 +
52 +## Garder deux opérateurs cohérents
53 +
54 +- **Docs, wrappers, procédures** : vivent dans `cluster-skill` (spbgit) → `git pull` sur chaque Mac.
55 +- **CLAUDE.md global et mémoire** : propres à chaque Mac. Après une session qui en a changé beaucoup, relancer `export-connaissances.sh --vers-cluster-skill` sur la machine source puis, sur l'autre, `installer-operateur.sh --depuis-cluster-skill` (importe depuis le clone, sans archive). Les mémoires ne sont pas fusionnées : la plus récente remplace (sauvegarde `~/.claude/backups/operateur-<date>/`).
56 +- **Clés** : une clé par machine, toutes autorisées partout ; pour révoquer une machine, retirer sa ligne des `authorized_keys` (`autoriser-operateur.sh --retirer "<clé>"`).
modified mld +1 −0
@@ -6,6 +6,7 @@ set -u
6 6 GW=M1M32
7 7 SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
8 8 if [ "${1:-}" = "stage" ]; then
9 + echo "⚠️ REMOTE-FIRST : \`mld stage\` ne sert qu'à CRÉER une app (stage → deploy → adopt). Pour modifier une app existante, travailler dans le dépôt git SUR LE NŒUD (mld status → ssh <nœud>) — voir MACLUSTR-PROCEDURE.md." >&2
9 10 src="${2:?usage: mld stage <dir-local> <app>}"; app="${3:?usage: mld stage <dir-local> <app>}"
10 11 # Un fichier .mldignore à la racine du projet ajoute des exclusions (syntaxe rsync) — p. ex. data/ avec des profils navigateur.
11 12 extra=(); [ -f "${src%/}/.mldignore" ] && extra=(--exclude-from="${src%/}/.mldignore")
added operateur/autoriser-operateur.sh +22 −0
@@ -0,0 +1,22 @@
1 +#!/bin/bash
2 +# autoriser-operateur.sh — autorise (ou retire) la clé publique d'un nouvel opérateur sur tous les nœuds MacLustr.
3 +# usage : autoriser-operateur.sh "<clé publique ssh-ed25519 …>" (ou chemin d'un fichier .pub)
4 +# autoriser-operateur.sh --retirer "<clé publique>"
5 +# À lancer depuis une machine déjà autorisée (laptop actuel ou M1M32). Cibles : les Macs de mld/config.py (NODES), BHS64 (ubuntu).
6 +set -uo pipefail
7 +MODE=add; [ "${1:-}" = "--retirer" ] && { MODE=rm; shift; }
8 +K="${1:?clé publique ou fichier .pub requis}"; [ -f "$K" ] && K=$(cat "$K")
9 +case "$K" in ssh-ed25519\ *|ssh-rsa\ *|ecdsa-*) ;; *) echo "clé publique invalide" >&2; exit 2;; esac
10 +NODES=$(ssh -o BatchMode=yes M1M32 "cd ~/maclustr-dispatch && python3 -c 'from mld import config; print(\" \".join(config.NODES))'") || { echo "M1M32 injoignable" >&2; exit 1; }
11 +ok=0; ko=""
12 +for n in $NODES BHS64; do
13 + if [ $MODE = add ]; then
14 + cmd="mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && (grep -qF '$K' ~/.ssh/authorized_keys || echo '$K' >> ~/.ssh/authorized_keys) && echo ok"
15 + else
16 + cmd="grep -vF '$K' ~/.ssh/authorized_keys > ~/.ssh/ak.tmp; mv ~/.ssh/ak.tmp ~/.ssh/authorized_keys; chmod 600 ~/.ssh/authorized_keys; echo ok"
17 + fi
18 + r=$(ssh -o BatchMode=yes -o ConnectTimeout=8 "$n" "$cmd" 2>/dev/null | tail -1)
19 + [ "$r" = ok ] && { ok=$((ok+1)); printf ' %-8s ok\n' "$n"; } || { ko="$ko $n"; printf ' %-8s INJOIGNABLE\n' "$n"; }
20 +done
21 +echo "$MODE : $ok nœud(s) traité(s)${ko:+ ; à refaire :$ko}"
22 +echo "Rappel : la nouvelle machine doit être dans le tailnet Tailscale (noms *.maclustr.io) et autorisée par l'ACL."
added operateur/export-connaissances.sh +38 −0
@@ -0,0 +1,38 @@
1 +#!/bin/bash
2 +# export-connaissances.sh — exporte la « connaissance » Claude Code + l'accès MacLustr de cet opérateur.
3 +# usage : export-connaissances.sh [--mcp] [--vers-cluster-skill] [chemin-archive.tar.gz]
4 +# --mcp inclut ~/.claude.json → mcpServers (JETONS !)
5 +# --vers-cluster-skill copie aussi CLAUDE.md + mémoire + commandes dans <cluster-skill>/operateur/connaissances/ et pousse sur spbgit
6 +set -euo pipefail
7 +MCP=0; VERS=0; OUT=""
8 +for a in "$@"; do case "$a" in --mcp) MCP=1;; --vers-cluster-skill) VERS=1;; *) OUT="$a";; esac; done
9 +OUT=${OUT:-$HOME/Desktop/maclustr-operateur-$(date +%Y%m%d).tar.gz}
10 +HERE="$(cd "$(dirname "$0")" && pwd)"
11 +SLUG=$(echo "$HOME" | tr '/' '-') # ex. -Users-simon-pierreboucher
12 +MEM="$HOME/.claude/projects/$SLUG/memory"
13 +T=$(mktemp -d); mkdir -p "$T/claude/memory" "$T/ssh" "$T/operateur"
14 +cp "$HOME/.claude/CLAUDE.md" "$T/claude/CLAUDE.md"
15 +[ -d "$HOME/.claude/commands" ] && cp -R "$HOME/.claude/commands" "$T/claude/"
16 +[ -f "$HOME/.claude/settings.json" ] && cp "$HOME/.claude/settings.json" "$T/claude/"
17 +[ -d "$MEM" ] && cp -R "$MEM/." "$T/claude/memory/" || echo "avertissement : pas de mémoire dans $MEM"
18 +cp "$HOME/.ssh/config" "$T/ssh/config"
19 +[ -f "$HOME/.ssh/maclustr-egress.sh" ] && cp "$HOME/.ssh/maclustr-egress.sh" "$T/ssh/"
20 +if [ $MCP = 1 ]; then
21 + python3 - "$T/claude/mcpServers.json" <<'PY'
22 +import json, os, sys
23 +d = json.load(open(os.path.expanduser("~/.claude.json")))
24 +json.dump(d.get("mcpServers", {}), open(sys.argv[1], "w"), indent=1)
25 +PY
26 +fi
27 +cp "$HERE"/*.sh "$T/operateur/" # les scripts voyagent avec l'archive
28 +echo "$HOME" > "$T/SOURCE_HOME"; whoami > "$T/SOURCE_USER"; date "+%Y-%m-%d %H:%M" > "$T/EXPORTED_AT"
29 +tar -czf "$OUT" -C "$T" . && chmod 600 "$OUT"
30 +echo "export → $OUT ($(du -h "$OUT" | cut -f1)) ; mémoire : $(ls "$T/claude/memory" | wc -l | tr -d ' ') fichiers ; MCP : $([ $MCP = 1 ] && echo inclus || echo non)"
31 +if [ $VERS = 1 ]; then
32 + CS="$(cd "$HERE/.." && pwd)"; K="$CS/operateur/connaissances"
33 + rm -rf "$K"; mkdir -p "$K"; cp -R "$T/claude/." "$K/"; rm -f "$K/mcpServers.json" # jamais les jetons dans git
34 + cp "$T/ssh/config" "$K/ssh-config"; [ -f "$T/ssh/maclustr-egress.sh" ] && cp "$T/ssh/maclustr-egress.sh" "$K/"
35 + cp "$T/SOURCE_HOME" "$T/EXPORTED_AT" "$K/"
36 + git -C "$CS" add operateur/connaissances && git -C "$CS" commit -q -m "connaissances Claude Code exportées depuis $(hostname -s) ($(date +%Y-%m-%d))" && git -C "$CS" push -q origin main && echo "poussé dans cluster-skill/operateur/connaissances/"
37 +fi
38 +rm -rf "$T"
added operateur/installer-operateur.sh +70 −0
@@ -0,0 +1,70 @@
1 +#!/bin/bash
2 +# installer-operateur.sh — installe sur CE Mac l'accès MacLustr + les connaissances Claude Code d'un opérateur.
3 +# usage : installer-operateur.sh <maclustr-operateur-<date>.tar.gz> (archive de export-connaissances.sh)
4 +# installer-operateur.sh --depuis-cluster-skill (importe depuis le clone cluster-skill déjà présent)
5 +# Idempotent : relancer après avoir fait autoriser la clé (autoriser-operateur.sh sur une machine déjà autorisée).
6 +set -uo pipefail
7 +ARC="${1:?archive .tar.gz ou --depuis-cluster-skill}"
8 +CS_DIR="$HOME/Desktop/Cluster/cluster-skill"; STAMP=$(date +%Y%m%d-%H%M); BK="$HOME/.claude/backups/operateur-$STAMP"
9 +T=$(mktemp -d)
10 +if [ "$ARC" = "--depuis-cluster-skill" ]; then
11 + [ -d "$CS_DIR/operateur/connaissances" ] || { echo "pas de $CS_DIR/operateur/connaissances (faire export-connaissances.sh --vers-cluster-skill sur la source)"; exit 1; }
12 + mkdir -p "$T/claude" "$T/ssh"; cp -R "$CS_DIR/operateur/connaissances/." "$T/claude/"; mv "$T/claude/ssh-config" "$T/ssh/config"; [ -f "$T/claude/maclustr-egress.sh" ] && mv "$T/claude/maclustr-egress.sh" "$T/ssh/"
13 + cp "$T/claude/SOURCE_HOME" "$T/SOURCE_HOME"
14 +else
15 + tar -xzf "$ARC" -C "$T" || { echo "archive illisible"; exit 1; }
16 +fi
17 +SRC_HOME=$(cat "$T/SOURCE_HOME"); echo "source : $SRC_HOME → cible : $HOME"
18 +remap(){ [ "$SRC_HOME" != "$HOME" ] && LC_ALL=C sed -i '' "s#$SRC_HOME#$HOME#g" "$@" 2>/dev/null || true; }
19 +
20 +echo "== 1. Prérequis"
21 +xcode-select -p >/dev/null 2>&1 || { echo " Command Line Tools absents → xcode-select --install (relancer ensuite)"; xcode-select --install 2>/dev/null; }
22 +if ! command -v brew >/dev/null; then echo " Homebrew absent : installer d'abord https://brew.sh puis relancer"; else
23 + for p in python3 rsync jq; do command -v $p >/dev/null || brew install -q $p; done; fi
24 +if command -v tailscale >/dev/null || [ -d /Applications/Tailscale.app ]; then
25 + ts=$( { tailscale status 2>/dev/null || /Applications/Tailscale.app/Contents/MacOS/Tailscale status 2>/dev/null; } | head -1)
26 + [ -n "$ts" ] && echo " Tailscale : $ts" || echo " Tailscale : installé, état inconnu (connecter l'app)"
27 +else echo " ⚠️ Tailscale absent : installer l'app et rejoindre le tailnet (les noms *.maclustr.io sont des IP Tailscale)"; fi
28 +
29 +echo "== 2. SSH"
30 +mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
31 +[ -f "$HOME/.ssh/config" ] && cp "$HOME/.ssh/config" "$HOME/.ssh/config.bak-operateur-$STAMP"
32 +cp "$T/ssh/config" "$HOME/.ssh/config" && chmod 600 "$HOME/.ssh/config" && remap "$HOME/.ssh/config"
33 +[ -f "$T/ssh/maclustr-egress.sh" ] && cp "$T/ssh/maclustr-egress.sh" "$HOME/.ssh/" && chmod +x "$HOME/.ssh/maclustr-egress.sh"
34 +if [ ! -f "$HOME/.ssh/id_ed25519" ]; then ssh-keygen -q -t ed25519 -N "" -C "maclustr-cluster ($(whoami)@$(hostname -s))" -f "$HOME/.ssh/id_ed25519"; echo " clé générée"; fi
35 +PUB=$(cat "$HOME/.ssh/id_ed25519.pub")
36 +echo " clé publique de ce Mac (à autoriser depuis une machine déjà autorisée) :"; echo " autoriser-operateur.sh \"$PUB\""
37 +
38 +echo "== 3. Claude Code"
39 +mkdir -p "$HOME/.claude" "$BK"
40 +[ -f "$HOME/.claude/CLAUDE.md" ] && cp "$HOME/.claude/CLAUDE.md" "$BK/"
41 +cp "$T/claude/CLAUDE.md" "$HOME/.claude/CLAUDE.md" && remap "$HOME/.claude/CLAUDE.md"
42 +[ -d "$T/claude/commands" ] && { mkdir -p "$HOME/.claude/commands"; cp -R "$T/claude/commands/." "$HOME/.claude/commands/"; }
43 +if [ -f "$T/claude/settings.json" ]; then
44 + if [ -f "$HOME/.claude/settings.json" ]; then cp "$T/claude/settings.json" "$HOME/.claude/settings.json.maclustr" && echo " settings.json existant conservé ; version source → settings.json.maclustr (fusion manuelle)"; else cp "$T/claude/settings.json" "$HOME/.claude/settings.json"; fi
45 +fi
46 +SLUG=$(echo "$HOME" | tr '/' '-'); MEM="$HOME/.claude/projects/$SLUG/memory"
47 +if [ -d "$T/claude/memory" ]; then
48 + [ -d "$MEM" ] && cp -R "$MEM" "$BK/memory"
49 + mkdir -p "$MEM" && cp -R "$T/claude/memory/." "$MEM/" && remap "$MEM"/*.md
50 + echo " mémoire : $(ls "$MEM"/*.md | wc -l | tr -d ' ') fichiers → $MEM"
51 +fi
52 +if [ -f "$T/claude/mcpServers.json" ]; then
53 + python3 - "$T/claude/mcpServers.json" <<'PY'
54 +import json, os, sys
55 +p = os.path.expanduser("~/.claude.json"); d = json.load(open(p)) if os.path.exists(p) else {}
56 +new = json.load(open(sys.argv[1])); cur = d.setdefault("mcpServers", {}); added = [k for k in new if k not in cur]
57 +for k in added: cur[k] = new[k]
58 +json.dump(d, open(p, "w"), indent=2); print(" serveurs MCP ajoutés :", ", ".join(added) or "aucun (déjà présents)")
59 +PY
60 +fi
61 +
62 +echo "== 4. cluster-skill"
63 +if ssh -o BatchMode=yes -o ConnectTimeout=8 gitsrv true 2>/dev/null; then
64 + if [ ! -d "$CS_DIR/.git" ]; then mkdir -p "$HOME/Desktop/Cluster" && git clone -q gitsrv:srv/git/cluster-skill.git "$CS_DIR" && echo " cloné → $CS_DIR"; else git -C "$CS_DIR" pull -q --ff-only && echo " mis à jour"; fi
65 + [ -e "$HOME/Desktop/cluster-skill" ] || ln -s "$CS_DIR" "$HOME/Desktop/cluster-skill"
66 + echo "== 5. Vérification"; ssh -o BatchMode=yes M1M32 hostname && "$CS_DIR/mld" status 2>/dev/null | head -5
67 +else
68 + echo " gitsrv (M1M32) injoignable : clé pas encore autorisée ou Tailscale absent → faire autoriser la clé ci-dessus puis RELANCER ce script."
69 +fi
70 +rm -rf "$T"; echo "terminé (sauvegardes : $BK)"
71