#!/usr/bin/env bash set -euo pipefail echo "Carlos — installation en cours…" CARLOS_HOME="$HOME/.carlos" RUNTIME="$CARLOS_HOME/runtime" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT mkdir -p "$CARLOS_HOME" "$RUNTIME" use_system=false use_private=false if [ -x "$CARLOS_HOME/node/bin/node" ] && [ -x "$CARLOS_HOME/node/bin/npm" ]; then private_major="$("$CARLOS_HOME/node/bin/node" -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)" if [ "$private_major" -ge 20 ]; then use_private=true; fi fi if command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1; then major="$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)" if [ "$major" -ge 20 ]; then use_system=true; fi fi if [ "$use_private" = true ]; then NODE_BIN="$CARLOS_HOME/node/bin/node" NPM_BIN="$CARLOS_HOME/node/bin/npm" elif [ "$use_system" = true ]; then NODE_BIN="$(command -v node)" NPM_BIN="$(command -v npm)" else case "$(uname -s)" in Darwin) platform=darwin ;; Linux) platform=linux ;; *) echo "Carlos: système non pris en charge par cet installateur."; exit 1 ;; esac case "$(uname -m)" in arm64|aarch64) arch=arm64 ;; x86_64|amd64) arch=x64 ;; *) echo "Carlos: architecture non prise en charge."; exit 1 ;; esac index="$(curl -fsSL https://nodejs.org/dist/latest-v22.x/)" file="$(printf '%s' "$index" | grep -oE "node-v[0-9.]+-$platform-$arch[.]tar[.]gz" | head -1)" [ -n "$file" ] || { echo "Carlos: runtime Node introuvable."; exit 1; } curl -fsSL "https://nodejs.org/dist/latest-v22.x/$file" -o "$TMP/$file" curl -fsSL https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt -o "$TMP/node-SHASUMS256.txt" node_expected="$(awk -v name="$file" '$2==name{print $1}' "$TMP/node-SHASUMS256.txt")" if command -v shasum >/dev/null 2>&1; then node_actual="$(shasum -a 256 "$TMP/$file" | cut -d ' ' -f 1)" elif command -v sha256sum >/dev/null 2>&1; then node_actual="$(sha256sum "$TMP/$file" | cut -d ' ' -f 1)" else echo "Carlos: utilitaire SHA-256 absent sur ce système."; exit 1 fi [ -n "$node_expected" ] && [ "$node_expected" = "$node_actual" ] || { echo "Carlos: archive Node.js non vérifiée."; exit 1; } tar -xzf "$TMP/$file" -C "$TMP" rm -rf "$CARLOS_HOME/node" folder="$(printf '%s' "$file" | sed 's/[.]tar[.]gz$//')" mv "$TMP/$folder" "$CARLOS_HOME/node" NODE_BIN="$CARLOS_HOME/node/bin/node" NPM_BIN="$CARLOS_HOME/node/bin/npm" fi export PATH="$(dirname "$NODE_BIN"):$PATH" echo "Carlos — installation de l'agent…" # Never downgrade a newer Carlos agent already present in the private runtime. EXISTING_PACKAGE="$RUNTIME/lib/node_modules/carlos-remote/package.json" SKIP_INSTALL=false if [ -r "$EXISTING_PACKAGE" ]; then EXISTING_VERSION="$("$NODE_BIN" -p 'try{require(process.argv[1]).version}catch{""}' "$EXISTING_PACKAGE" 2>/dev/null || true)" if "$NODE_BIN" -e 'const score=s=>/^\d+\.\d+\.\d+$/.test(s)?s.split(".").reduce((n,v)=>n*1000+Number(v),0):-1;process.exit(score(process.argv[1])>score(process.argv[2])?0:1)' "$EXISTING_VERSION" "0.17.10"; then SKIP_INSTALL=true echo "Carlos — version plus récente conservée : $EXISTING_VERSION" fi fi if [ "$SKIP_INSTALL" = false ]; then PACKAGE="$TMP/carlos-agent.tgz" curl -fsSL https://www.usecarlos.com/downloads/carlos-agent-0.17.10.tgz -o "$PACKAGE" CHECKSUM="$(curl -fsSL https://www.usecarlos.com/downloads/carlos-agent-0.17.10.tgz.sha256)" EXPECTED="$(printf '%s' "$CHECKSUM" | cut -d ' ' -f 1)" [[ "$EXPECTED" =~ ^[a-f0-9]{64}$ ]] || { echo "Carlos: empreinte de distribution invalide."; exit 1; } ACTUAL="$("$NODE_BIN" -e 'const fs=require("fs"),crypto=require("crypto");process.stdout.write(crypto.createHash("sha256").update(fs.readFileSync(process.argv[1])).digest("hex"))' "$PACKAGE")" [ "$ACTUAL" = "$EXPECTED" ] || { echo "Carlos: archive modifiée ou téléchargement corrompu."; exit 1; } "$NPM_BIN" install -g --prefix "$RUNTIME" "$PACKAGE" --silent fi CARLOS="$RUNTIME/bin/carlos" [ -x "$CARLOS" ] || { echo "Carlos: installation incomplète."; exit 1; } # A stable launcher pins Node, including when it was downloaded by this installer. BIN="$CARLOS_HOME/bin" mkdir -p "$BIN" printf '#!/usr/bin/env bash\nexec %q %q "$@"\n' "$NODE_BIN" "$RUNTIME/lib/node_modules/carlos-remote/cli.mjs" > "$BIN/carlos" chmod 700 "$BIN/carlos" cp "$BIN/carlos" "$BIN/carlos-remote" export PATH="$BIN:$PATH" PATH_LINE='export PATH="$HOME/.carlos/bin:$PATH"' register_profile(){ profile="$1" if ! grep -Fqx "$PATH_LINE" "$profile" 2>/dev/null; then printf '\n# Carlos Remote\n%s\n' "$PATH_LINE" >> "$profile" fi } case "$(uname -s)" in Darwin) register_profile "$HOME/.zprofile" register_profile "$HOME/.zshrc" register_profile "$HOME/.bash_profile" register_profile "$HOME/.bashrc" ;; *) register_profile "$HOME/.profile" register_profile "$HOME/.bashrc" register_profile "$HOME/.zshrc" ;; esac CARLOS="$BIN/carlos" echo "Carlos — agent Remote installé." echo "Carlos — autorise cet ordinateur dans la page qui va s'ouvrir." echo "Carlos — le LLM peut rester côté cloud. Aucun modèle IA local n’est obligatoire pour installer la couche Remote." "$CARLOS" remote --install-service echo "" echo "────────────────────────────────────────────" echo " EXTENSIONS FACULTATIVES" echo "────────────────────────────────────────────" echo " Pour contrôler un profil Chromium existant : carlos browser-setup" echo " Tu peux le faire plus tard, sans réinstaller Carlos." "$CARLOS" doctor echo "Pour un nouveau terminal : carlos doctor" echo "Dans ce terminal actuel : $CARLOS doctor" echo 'Pour autoriser un autre dossier : carlos permissions --allow "/chemin/du/dossier"' echo "Carlos Remote est installé : agent + service. Connecte maintenant le LLM de ton choix à Carlos."