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
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 | ||