Last active
July 19, 2026 11:59
-
-
Save lastknight/f21271d761a86a5ab2b6a5f2e73256d5 to your computer and use it in GitHub Desktop.
clouded-bootstrap: one-curl Claude Code setup with skills
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # clouded-bootstrap.sh — Setup/migrazione Claude Code su qualsiasi macchina | |
| # Idempotente: funziona su macchina nuova E su macchina esistente da aggiornare. | |
| # Usage: curl -fsSL https://gist.githubusercontent.com/lastknight/f21271d761a86a5ab2b6a5f2e73256d5/raw/clouded-bootstrap.sh | bash | |
| set -euo pipefail | |
| # ── colori ──────────────────────────────────────────────────────────────────── | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BOLD='\033[1m'; NC='\033[0m' | |
| log() { echo -e "${GREEN}▸${NC} $*"; } | |
| warn() { echo -e "${YELLOW}⚠${NC} $*"; } | |
| err() { echo -e "${RED}✗${NC} $*" >&2; exit 1; } | |
| ok() { echo -e "${GREEN}✓${NC} $*"; } | |
| REPO="lastknight/claude-skills" | |
| SKILLS_DIR="$HOME/.claude/skills" | |
| REPO_DIR="$HOME/.claude-skills-repo" | |
| HOSTNAME_SHORT="$(hostname -s 2>/dev/null || hostname)" | |
| CMD_NAME="clouded" | |
| MEMORY_DIR="$HOME/.claude/projects/$(echo "$HOME" | sed 's|/|-|g')/memory" | |
| # rsync exclusions, MUST match sync.md (single source of truth) | |
| # NB: '/scripts/' ANCORATO alla root del repo: un pattern non ancorato ('scripts/') | |
| # escluderebbe anche health/scripts/, md-to-docx/scripts/, rm/scripts/ rompendo | |
| # le skill al primo bootstrap (bug trovato dall'architect il 2026-07-12). | |
| # 'config/' NON si esclude: va in ~/.claude/skills/config/ come da tabella sync.md. | |
| RSYNC_EXCLUDES=( | |
| --exclude='.git' --exclude='.DS_Store' --exclude='.gitignore' | |
| --exclude='README.md' --exclude='README.pdf' --exclude='CLAUDE.md' | |
| --exclude='memory/' --exclude='agents/' --exclude='rules/' | |
| --exclude='sessions/' --exclude='hooks/' --exclude='/scripts/' | |
| --exclude='settings.json' --exclude='rm/state/' | |
| --exclude='clouded-bootstrap.sh' --exclude='clouded-refresh.sh' | |
| ) | |
| # rileva se siamo su server headless (SSH senza display) | |
| HAS_DISPLAY=false | |
| if [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ] || [[ "$(uname)" == "Darwin" ]]; then | |
| HAS_DISPLAY=true | |
| fi | |
| # rileva se siamo root | |
| IS_ROOT=false | |
| [ "$(id -u)" = "0" ] && IS_ROOT=true | |
| echo -e "\n${BOLD}clouded bootstrap${NC} — macchina: ${YELLOW}${HOSTNAME_SHORT}${NC}\n" | |
| # ── 1. prerequisiti base ────────────────────────────────────────────────────── | |
| log "Controllo prerequisiti..." | |
| check_cmd() { | |
| command -v "$1" &>/dev/null && return 0 | |
| return 1 | |
| } | |
| # funzione per installare pacchetti di sistema (apt o brew) | |
| install_pkg() { | |
| if check_cmd apt-get; then | |
| apt-get install -y "$@" 2>/dev/null || warn "Impossibile installare $* (serve root?)" | |
| elif check_cmd brew; then | |
| brew install "$@" | |
| else | |
| warn "Impossibile installare $* — installa manualmente" | |
| fi | |
| } | |
| # claude | |
| if ! check_cmd claude; then | |
| warn "Claude Code non trovato. Provo ad installarlo..." | |
| if check_cmd npm; then | |
| npm install -g @anthropic-ai/claude-code | |
| elif check_cmd brew; then | |
| brew install claude | |
| else | |
| err "npm non trovato. Installa Node.js prima:\n https://nodejs.org\nPoi: npm install -g @anthropic-ai/claude-code" | |
| fi | |
| fi | |
| ok "claude $(claude --version 2>/dev/null | head -1)" | |
| # gh CLI | |
| if ! check_cmd gh; then | |
| warn "GitHub CLI (gh) non trovato. Provo ad installarlo..." | |
| if check_cmd brew; then | |
| brew install gh | |
| elif check_cmd apt-get; then | |
| (type -p curl >/dev/null || apt install curl -y) && \ | |
| curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg 2>/dev/null && \ | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \ | |
| apt update && apt install gh -y | |
| else | |
| err "Installa gh manualmente: https://cli.github.com" | |
| fi | |
| fi | |
| ok "gh $(gh --version 2>/dev/null | head -1 | awk '{print $3}')" | |
| # rsync, git, tmux, curl | |
| for tool in rsync git tmux curl; do | |
| if ! check_cmd "$tool"; then | |
| warn "$tool non trovato. Installo..." | |
| install_pkg "$tool" | |
| fi | |
| check_cmd "$tool" && ok "$tool" || warn "$tool non disponibile" | |
| done | |
| # ── 1b. dipendenze Python e system ─────────────────────────────────────────── | |
| log "Installazione dipendenze Python e system tools..." | |
| # poppler-utils (per lettura PDF) | |
| if ! check_cmd pdftotext; then | |
| log "Installo poppler-utils (lettura PDF)..." | |
| install_pkg poppler-utils | |
| fi | |
| check_cmd pdftotext && ok "poppler-utils (pdftotext)" || warn "poppler-utils non installato" | |
| # Python3 packages per md-to-docx e conversioni | |
| if check_cmd python3; then | |
| ok "python3 $(python3 --version 2>&1 | awk '{print $2}')" | |
| # controlla se i pacchetti sono già installati | |
| MISSING_PY="" | |
| python3 -c "import yaml" 2>/dev/null || MISSING_PY="$MISSING_PY python3-yaml" | |
| python3 -c "import markdown" 2>/dev/null || MISSING_PY="$MISSING_PY python3-markdown" | |
| python3 -c "import bs4" 2>/dev/null || MISSING_PY="$MISSING_PY python3-bs4" | |
| python3 -c "import docx" 2>/dev/null || MISSING_PY="$MISSING_PY python3-docx" | |
| if [ -n "$MISSING_PY" ]; then | |
| log "Installo pacchetti Python:$MISSING_PY" | |
| if check_cmd apt-get; then | |
| apt-get install -y $MISSING_PY 2>/dev/null || \ | |
| pip3 install --break-system-packages pyyaml python-docx markdown beautifulsoup4 2>/dev/null || \ | |
| warn "Impossibile installare pacchetti Python (serve root?)" | |
| elif check_cmd pip3; then | |
| pip3 install pyyaml python-docx markdown beautifulsoup4 | |
| elif check_cmd brew; then | |
| pip3 install pyyaml python-docx markdown beautifulsoup4 | |
| fi | |
| fi | |
| # verifica | |
| python3 -c "import yaml, markdown, bs4, docx" 2>/dev/null && ok "Python packages (yaml, markdown, bs4, docx)" || warn "Alcuni pacchetti Python mancanti" | |
| else | |
| warn "python3 non trovato — md-to-docx non funzionerà" | |
| fi | |
| # ── 1c. uv (per /rm skill e altre skill con Python deps isolate) ───────────── | |
| if ! check_cmd uv; then | |
| log "Installo uv (Python package runner per skill /rm e altre)..." | |
| curl -LsSf https://astral.sh/uv/install.sh | sh 2>/dev/null && \ | |
| export PATH="$HOME/.local/bin:$PATH" | |
| fi | |
| check_cmd uv && ok "uv $(uv --version 2>&1 | head -1)" || warn "uv non installato — /rm non funzionerà" | |
| # ── 1d. rmapi (Remarkable Cloud CLI per skill /rm) ─────────────────────────── | |
| # v0.0.32 fork ddvk (supporta v6 .lines del Paper Pro Move) | |
| RMAPI_BIN="$HOME/bin/rmapi" | |
| if [ ! -x "$RMAPI_BIN" ]; then | |
| log "Installo rmapi v0.0.32 (ddvk fork) per skill /rm..." | |
| mkdir -p "$HOME/bin" | |
| ARCH=$(uname -m) | |
| OS=$(uname -s | tr '[:upper:]' '[:lower:]') | |
| case "$OS-$ARCH" in | |
| linux-x86_64) RMAPI_URL="https://github.com/ddvk/rmapi/releases/download/v0.0.32/rmapi-linux-amd64.tar.gz" ;; | |
| linux-aarch64) RMAPI_URL="https://github.com/ddvk/rmapi/releases/download/v0.0.32/rmapi-linux-arm64.tar.gz" ;; | |
| darwin-arm64) RMAPI_URL="https://github.com/ddvk/rmapi/releases/download/v0.0.32/rmapi-macos-arm64.zip" ;; | |
| darwin-x86_64) RMAPI_URL="https://github.com/ddvk/rmapi/releases/download/v0.0.32/rmapi-macos-intel.zip" ;; | |
| *) warn "Architettura non supportata da rmapi: $OS-$ARCH"; RMAPI_URL="" ;; | |
| esac | |
| if [ -n "$RMAPI_URL" ]; then | |
| TMPDIR=$(mktemp -d) | |
| curl -sL -o "$TMPDIR/rmapi.pkg" "$RMAPI_URL" | |
| if [[ "$RMAPI_URL" == *.zip ]]; then | |
| unzip -q "$TMPDIR/rmapi.pkg" -d "$HOME/bin/" | |
| else | |
| tar -xzf "$TMPDIR/rmapi.pkg" -C "$HOME/bin/" | |
| fi | |
| chmod +x "$RMAPI_BIN" | |
| rm -rf "$TMPDIR" | |
| fi | |
| fi | |
| if [ -x "$RMAPI_BIN" ]; then | |
| ok "rmapi $($RMAPI_BIN version 2>&1 | head -1)" | |
| # Pairing: se non c'è token, istruisci l'utente (NON automatizzabile, codice one-time) | |
| if [ ! -f "$HOME/.config/rmapi/rmapi.conf" ]; then | |
| warn "rmapi non ancora autenticato." | |
| warn " 1) Apri https://my.remarkable.com/device/desktop/connect" | |
| warn " 2) Copia il codice di 8 caratteri" | |
| warn " 3) Esegui: echo '<codice>' | ~/bin/rmapi" | |
| warn " Esecuzione one-time. Dopo rmapi è autenticato per sempre (refresh token)." | |
| fi | |
| else | |
| warn "rmapi non installato — skill /rm non funzionerà" | |
| fi | |
| # ── 2. autenticazione gh ────────────────────────────────────────────────────── | |
| log "Verifica autenticazione GitHub..." | |
| if ! gh auth status &>/dev/null; then | |
| warn "Non autenticato con gh. Avvio autenticazione..." | |
| gh auth login | |
| fi | |
| ok "GitHub autenticato" | |
| gh auth setup-git 2>/dev/null || true | |
| # ── 3. skills ───────────────────────────────────────────────────────────────── | |
| log "Download skills da ${REPO}..." | |
| git config --global --add safe.directory "$REPO_DIR" 2>/dev/null || true | |
| if [ -d "$REPO_DIR/.git" ] && [ "$(stat -c '%u' "$REPO_DIR" 2>/dev/null || stat -f '%u' "$REPO_DIR" 2>/dev/null)" = "$(id -u)" ]; then | |
| cd "$REPO_DIR" && git pull --quiet | |
| else | |
| rm -rf "$REPO_DIR" | |
| gh repo clone "$REPO" "$REPO_DIR" -- --quiet | |
| fi | |
| mkdir -p "$SKILLS_DIR" | |
| # ── 3a. CLEANUP — rimuovi directory che non devono stare sotto skills/ ─────── | |
| log "Cleanup directory obsolete..." | |
| CLEANED=0 | |
| # Skill deprecate (migrate a /self nel marzo 2026) | |
| for old_dir in warmup sogna coherence person-decode skills; do | |
| if [ -d "$SKILLS_DIR/$old_dir" ]; then | |
| rm -rf "$SKILLS_DIR/$old_dir" | |
| ok "Rimosso: $old_dir/ (migrato a /self)" | |
| CLEANED=$((CLEANED + 1)) | |
| fi | |
| done | |
| # Directory del repo finite in skills/ per errore (vecchi rsync senza esclusioni) | |
| for stray_dir in agents hooks rules memory sessions scripts config; do | |
| if [ -d "$SKILLS_DIR/$stray_dir" ]; then | |
| rm -rf "$SKILLS_DIR/$stray_dir" | |
| ok "Rimosso: $stray_dir/ (non deve stare in skills/)" | |
| CLEANED=$((CLEANED + 1)) | |
| fi | |
| done | |
| # File del repo finiti in skills/ per errore | |
| for stray_file in clouded-bootstrap.sh settings.json README.pdf README.md; do | |
| if [ -f "$SKILLS_DIR/$stray_file" ]; then | |
| rm -f "$SKILLS_DIR/$stray_file" | |
| ok "Rimosso: $stray_file (non deve stare in skills/)" | |
| CLEANED=$((CLEANED + 1)) | |
| fi | |
| done | |
| [ "$CLEANED" -gt 0 ] && ok "Cleanup: $CLEANED elementi rimossi" || ok "Nessun residuo trovato" | |
| # ── 3b. salva stile attivo se esiste ───────────────────────────────────────── | |
| ACTIVE_STYLE="" | |
| if [ -L "$SKILLS_DIR/ACTIVE_STYLE.md" ]; then | |
| ACTIVE_STYLE="$(readlink "$SKILLS_DIR/ACTIVE_STYLE.md")" | |
| fi | |
| # ── 3c. rsync skills (con esclusioni corrette) ────────────────────────────── | |
| rsync -a --delete "${RSYNC_EXCLUDES[@]}" "$REPO_DIR/" "$SKILLS_DIR/" | |
| ok "Skills sincronizzate" | |
| # ── 3d. agents, rules ──────────────────────────────────────────────────────── | |
| mkdir -p "$HOME/.claude/agents" "$HOME/.claude/rules" | |
| [ -d "$REPO_DIR/agents" ] && rsync -a "$REPO_DIR/agents/" "$HOME/.claude/agents/" | |
| [ -d "$REPO_DIR/rules" ] && rsync -a "$REPO_DIR/rules/" "$HOME/.claude/rules/" | |
| ok "Agents e rules sincronizzati" | |
| # ── 3e. CLAUDE.md globale ──────────────────────────────────────────────────── | |
| [ -f "$REPO_DIR/CLAUDE.md" ] && cp "$REPO_DIR/CLAUDE.md" "$HOME/.claude/CLAUDE.md" | |
| ok "CLAUDE.md aggiornato" | |
| # ── 3f. hooks ──────────────────────────────────────────────────────────────── | |
| if [ -d "$REPO_DIR/hooks" ]; then | |
| mkdir -p "$HOME/.claude/hooks" | |
| cp "$REPO_DIR/hooks/"*.sh "$HOME/.claude/hooks/" 2>/dev/null | |
| chmod +x "$HOME/.claude/hooks/"*.sh 2>/dev/null | |
| ok "Hooks sincronizzati" | |
| fi | |
| # ── 3g. settings.json ─────────────────────────────────────────────────────── | |
| [ -f "$REPO_DIR/settings.json" ] && cp "$REPO_DIR/settings.json" "$HOME/.claude/settings.json" | |
| # ── 3h. ripristina stile ──────────────────────────────────────────────────── | |
| if [ -n "$ACTIVE_STYLE" ]; then | |
| ln -sf "$ACTIVE_STYLE" "$SKILLS_DIR/ACTIVE_STYLE.md" | |
| elif [ -f "$SKILLS_DIR/styles/tf-corporate.md" ]; then | |
| ln -sf "styles/tf-corporate.md" "$SKILLS_DIR/ACTIVE_STYLE.md" | |
| fi | |
| ok "Stile attivo: $(readlink "$SKILLS_DIR/ACTIVE_STYLE.md" 2>/dev/null || echo 'nessuno')" | |
| # ── 3i. memory ─────────────────────────────────────────────────────────────── | |
| log "Sincronizzazione memorie..." | |
| mkdir -p "$MEMORY_DIR" | |
| if [ -d "$REPO_DIR/memory" ]; then | |
| rsync -a "$REPO_DIR/memory/" "$MEMORY_DIR/" | |
| ok "Memorie sincronizzate in $MEMORY_DIR" | |
| else | |
| warn "Nessuna directory memory nel repo — skip" | |
| fi | |
| # ── 3j. copia repo in /tmp per sync futuro ─────────────────────────────────── | |
| if [ ! -d /tmp/claude-skills/.git ]; then | |
| cp -a "$REPO_DIR" /tmp/claude-skills | |
| ok "Repo copiato in /tmp/claude-skills per /self update" | |
| fi | |
| # ── 4. comando 'clouded' ────────────────────────────────────────────────────── | |
| log "Installazione comando '${CMD_NAME}'..." | |
| # sceglie la directory di installazione | |
| if [ -w /usr/local/bin ]; then | |
| INSTALL_DIR="/usr/local/bin" | |
| elif [ -d "$HOME/.local/bin" ]; then | |
| INSTALL_DIR="$HOME/.local/bin" | |
| else | |
| mkdir -p "$HOME/.local/bin" | |
| INSTALL_DIR="$HOME/.local/bin" | |
| fi | |
| CMD_PATH="$INSTALL_DIR/$CMD_NAME" | |
| cat > "$CMD_PATH" << 'ENDOFSCRIPT' | |
| #!/usr/bin/env bash | |
| # clouded — Claude Code con tmux persistente + Telegram | |
| SESSION="clouded" | |
| CLAUDE_CMD="claude --dangerously-skip-permissions --channels plugin:telegram@claude-plugins-official" | |
| if command -v tmux &>/dev/null; then | |
| if tmux has-session -t "$SESSION" 2>/dev/null; then | |
| exec tmux attach-session -t "$SESSION" | |
| else | |
| tmux new-session -d -s "$SESSION" "while true; do $CLAUDE_CMD; echo 'Claude uscito. Riavvio in 2s... (Ctrl+C per fermare)'; sleep 2; done" | |
| exec tmux attach-session -t "$SESSION" | |
| fi | |
| else | |
| exec $CLAUDE_CMD | |
| fi | |
| ENDOFSCRIPT | |
| chmod +x "$CMD_PATH" | |
| ok "Comando installato: $CMD_PATH (con Telegram)" | |
| # ── 4b. tmux: Shift+Invio come a-capo in Claude Code (extended keys) ─────── | |
| # clouded gira dentro tmux: senza extended-keys tmux non inoltra Shift+Invio | |
| # (CSI u) e Claude Code lo riceve come Invio normale. Idempotente. | |
| if ! grep -q "extended-keys" "$HOME/.tmux.conf" 2>/dev/null; then | |
| printf '# Claude Code: Shift+Invio = a-capo attraverso tmux (extended keys / CSI u)\nset -s extended-keys on\nset -as terminal-features \x27xterm*:extkeys\x27\n' >> "$HOME/.tmux.conf" | |
| fi | |
| # applica anche al server tmux già in esecuzione, se c'è | |
| tmux set -s extended-keys on 2>/dev/null || true | |
| tmux set -as terminal-features 'xterm*:extkeys' 2>/dev/null || true | |
| # ── 5. PATH check ───────────────────────────────────────────────────────────── | |
| if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then | |
| warn "$INSTALL_DIR non è nel PATH." | |
| SHELL_RC="" | |
| case "${SHELL:-}" in | |
| */zsh) SHELL_RC="$HOME/.zshrc" ;; | |
| */bash) SHELL_RC="$HOME/.bashrc" ;; | |
| esac | |
| if [ -n "$SHELL_RC" ] && ! grep -q "$INSTALL_DIR" "$SHELL_RC" 2>/dev/null; then | |
| echo "" >> "$SHELL_RC" | |
| echo "# clouded" >> "$SHELL_RC" | |
| echo "export PATH=\"$INSTALL_DIR:\$PATH\"" >> "$SHELL_RC" | |
| ok "PATH aggiunto a $SHELL_RC" | |
| fi | |
| fi | |
| # ── 5a-bis. Locale UTF-8 (fix encoding Claude Code) ───────────────────────── | |
| # Il locale va PRIMA del guard non-interattivo nel .bashrc, altrimenti | |
| # Claude Code (shell non-interattiva) non lo vede e rompe gli accenti. | |
| SHELL_RC="" | |
| case "${SHELL:-}" in | |
| */zsh) SHELL_RC="$HOME/.zshrc" ;; | |
| */bash) SHELL_RC="$HOME/.bashrc" ;; | |
| esac | |
| if [ -n "$SHELL_RC" ]; then | |
| if ! grep -q 'export LANG=it_IT.UTF-8' "$SHELL_RC" 2>/dev/null; then | |
| # Inserisci in testa al file (prima del guard non-interattivo) | |
| sed -i '1i export LANG=it_IT.UTF-8\nexport LC_ALL=it_IT.UTF-8' "$SHELL_RC" | |
| ok "Locale UTF-8 aggiunto in testa a $SHELL_RC" | |
| fi | |
| fi | |
| # Genera il locale se non esiste | |
| if ! locale -a 2>/dev/null | grep -q 'it_IT.utf8'; then | |
| if [ -f /etc/locale.gen ]; then | |
| sed -i 's/^# *it_IT.UTF-8/it_IT.UTF-8/' /etc/locale.gen | |
| locale-gen 2>/dev/null && ok "Locale it_IT.UTF-8 generato" || warn "locale-gen fallito (non root?)" | |
| fi | |
| fi | |
| # ── 5b. scripts operativi ──────────────────────────────────────────────────── | |
| if [ -d "$REPO_DIR/scripts" ]; then | |
| mkdir -p "$INSTALL_DIR" | |
| cp "$REPO_DIR/scripts/"*.sh "$INSTALL_DIR/" 2>/dev/null | |
| chmod +x "$INSTALL_DIR/"*.sh 2>/dev/null | |
| ok "Scripts operativi installati" | |
| fi | |
| # ── 5c. (rimosso) cron sogna-nightly ────────────────────────────────────────── | |
| # Il cron sogna-nightly e' stato rimosso il 2026-06-19: il consolidamento resta | |
| # manuale via /self dream (o /self update). Nessuno script sogna-nightly.sh nel repo, | |
| # quindi questa sezione non installa nulla (prima usciva con exit 1 e rompeva il bootstrap). | |
| # ── 5d. cron report automatici (health-morning + newsletter-report) ─────────── | |
| # Gli script sono gia' stati copiati in $INSTALL_DIR da §5b (repo/scripts/*.sh). | |
| # Qui registriamo solo i cron, in modo idempotente. | |
| log "Installazione cron report (health + newsletter)..." | |
| HEALTH_PATH="$INSTALL_DIR/health-morning.sh" | |
| NEWS_PATH="$INSTALL_DIR/newsletter-report.sh" | |
| if check_cmd crontab; then | |
| (crontab -l 2>/dev/null | grep -v -e health-morning -e newsletter-report; \ | |
| echo "0 7 * * * $HEALTH_PATH"; \ | |
| echo "0 7 * * 2 $NEWS_PATH") | crontab - | |
| ok "Cron report installati: health giornaliero 09:00 Roma, newsletter martedì 09:00 Roma" | |
| else | |
| warn "crontab non disponibile — installa i cron report manualmente" | |
| fi | |
| # ── 5e. drive-backup (Google Drive via gog) ─────────────────────────────────── | |
| # Lo script e' gia' copiato in $INSTALL_DIR da §5b. Niente cron: gira dentro | |
| # /self update (push step 11). Qui verifichiamo solo che sia operativo. | |
| if [ -x "$INSTALL_DIR/drive-backup.sh" ]; then | |
| if check_cmd gog; then | |
| ok "drive-backup.sh installato (root Drive: 1-24213U8EedhOI11ZRpC8LHn-5Z4NLQ9, gira in /self update)" | |
| else | |
| warn "drive-backup.sh presente ma gog non configurato — il backup Drive non funzionera' finche' gog non e' autenticato" | |
| fi | |
| else | |
| warn "drive-backup.sh NON installato — controllare repo/scripts/" | |
| fi | |
| # ── 6. MCP: Microsoft 365 Calendar (globale, non npx) ───────────────────────── | |
| echo "" | |
| log "Configurazione Microsoft 365 Calendar..." | |
| # installa globalmente (NON npx — il token si perde con npx) | |
| if ! check_cmd ms-365-mcp-server; then | |
| log "Installo ms-365-mcp-server globalmente..." | |
| npm install -g @softeria/ms-365-mcp-server 2>/dev/null || warn "Impossibile installare ms-365-mcp-server (serve root per npm global?)" | |
| fi | |
| if check_cmd ms-365-mcp-server; then | |
| ok "ms-365-mcp-server installato globalmente" | |
| # chmod per permettere scrittura token dall'utente claude | |
| MS365_DIR="$(npm root -g 2>/dev/null)/@softeria/ms-365-mcp-server" | |
| if [ -d "$MS365_DIR" ] && $IS_ROOT; then | |
| chmod 777 "$MS365_DIR" 2>/dev/null || true | |
| ok "Permessi token directory impostati" | |
| elif [ -d "$MS365_DIR" ]; then | |
| warn "Token directory potrebbe non essere scrivibile (serve chmod 777 da root su $MS365_DIR)" | |
| fi | |
| # autenticazione (device code flow — funziona anche headless) | |
| echo "" | |
| log "Autenticazione Microsoft 365 (device code flow)..." | |
| echo -e "${YELLOW}Verrà mostrato un URL e un codice. Apri l'URL in un browser qualsiasi e inserisci il codice.${NC}" | |
| read -r -p "Procedere? [y/N] " choice | |
| if [[ "$choice" =~ ^[Yy]$ ]]; then | |
| ms-365-mcp-server --login --org-mode && ok "Microsoft 365 autenticato" || warn "Autenticazione saltata" | |
| else | |
| warn "Autenticazione saltata. Quando vuoi: ms-365-mcp-server --login --org-mode" | |
| fi | |
| else | |
| warn "ms-365-mcp-server non installato — calendario non disponibile" | |
| fi | |
| # ── 7. Gmail: gog CLI ───────────────────────────────────────────────────────── | |
| echo "" | |
| log "Configurazione Gmail (gog)..." | |
| if ! check_cmd gog; then | |
| warn "gog non trovato. Provo ad installarlo..." | |
| if check_cmd brew; then | |
| brew install steipete/tap/gogcli | |
| else | |
| GOG_VERSION="0.12.0" | |
| ARCH="$(uname -m)" | |
| OS="$(uname -s | tr '[:upper:]' '[:lower:]')" | |
| case "$ARCH" in | |
| x86_64) ARCH="amd64" ;; | |
| aarch64|arm64) ARCH="arm64" ;; | |
| esac | |
| GOG_URL="https://github.com/steipete/gogcli/releases/download/v${GOG_VERSION}/gogcli_${GOG_VERSION}_${OS}_${ARCH}.tar.gz" | |
| if [ -w /usr/local/bin ]; then | |
| curl -fsSL "$GOG_URL" | tar -xz -C /usr/local/bin gog | |
| chmod +x /usr/local/bin/gog | |
| else | |
| mkdir -p "$HOME/.local/bin" | |
| curl -fsSL "$GOG_URL" | tar -xz -C "$HOME/.local/bin" gog | |
| chmod +x "$HOME/.local/bin/gog" | |
| fi | |
| fi | |
| fi | |
| check_cmd gog && ok "gog $(gog --version 2>/dev/null | head -1)" || warn "gog non installato — Gmail non disponibile" | |
| # registra credenziali e token | |
| if check_cmd gog; then | |
| GOG_CREDS="$REPO_DIR/config/gog-credentials.json" | |
| GOG_TOKEN="$REPO_DIR/config/gog-token.json" | |
| if [ -f "$GOG_CREDS" ]; then | |
| gog auth credentials set "$GOG_CREDS" 2>/dev/null && ok "Credenziali Gmail registrate" | |
| else | |
| warn "gog-credentials.json non trovato — skip" | |
| fi | |
| if [ -f "$GOG_TOKEN" ]; then | |
| export GOG_KEYRING_PASSWORD=clouded | |
| gog auth keyring file 2>/dev/null || true | |
| gog auth tokens import "$GOG_TOKEN" 2>/dev/null && ok "Token Gmail importato" | |
| grep -q GOG_KEYRING_PASSWORD "$HOME/.bashrc" 2>/dev/null || echo 'export GOG_KEYRING_PASSWORD=clouded' >> "$HOME/.bashrc" | |
| # Env gog per shell NON interattive (cron/hook non caricano .bashrc): drive-backup.sh la sourcia | |
| mkdir -p "$HOME/.config/gog" | |
| printf 'export GOG_KEYRING_PASSWORD=clouded\nexport GOG_ACCOUNT=mf@matteoflora.com\n' > "$HOME/.config/gog/env" | |
| chmod 600 "$HOME/.config/gog/env" | |
| ok "Env gog non-interattiva in ~/.config/gog/env" | |
| else | |
| if $HAS_DISPLAY; then | |
| read -r -p "Autenticarsi con Gmail ora? (richiede browser) [y/N] " choice | |
| [[ "$choice" =~ ^[Yy]$ ]] && gog auth add mf@matteoflora.com && ok "Gmail autenticato" | |
| else | |
| warn "Server headless: esegui 'gog auth tokens import <file>' per autenticare Gmail" | |
| fi | |
| fi | |
| fi | |
| # ── 7b. rclone: motore drive-backup (sync ~/work su Google Drive) ───────────── | |
| echo "" | |
| log "Configurazione rclone (drive-backup)..." | |
| if ! check_cmd rclone; then | |
| if check_cmd brew; then brew install rclone | |
| elif check_cmd apt-get; then sudo apt-get install -y rclone 2>/dev/null || apt-get install -y rclone 2>/dev/null || true | |
| fi | |
| fi | |
| if check_cmd rclone; then | |
| ok "rclone $(rclone version 2>/dev/null | head -1)" | |
| if ! rclone listremotes 2>/dev/null | grep -q '^gdrive:'; then | |
| warn "Remote 'gdrive' mancante: serve OAuth manuale (una tantum per macchina)." | |
| warn "Da macchina con browser: rclone authorize \"drive\" → poi qui:" | |
| warn "rclone config create gdrive drive scope=drive root_folder_id=1-24213U8EedhOI11ZRpC8LHn-5Z4NLQ9 token '<blob>' --non-interactive" | |
| warn "Senza remote, drive-backup usa il fallback gog (funziona comunque)." | |
| fi | |
| else | |
| warn "rclone non installato — drive-backup userà il fallback gog" | |
| fi | |
| # ── 8. utente 'claude' per SSH (solo se root) ───────────────────────────────── | |
| if $IS_ROOT; then | |
| echo "" | |
| log "Creazione utente 'claude' per SSH..." | |
| if ! id claude &>/dev/null; then | |
| useradd -m -s /bin/bash claude | |
| ok "Utente 'claude' creato" | |
| else | |
| ok "Utente 'claude' già esistente" | |
| fi | |
| # copia le authorized_keys di root | |
| mkdir -p /home/claude/.ssh | |
| if [ -f /root/.ssh/authorized_keys ]; then | |
| cp /root/.ssh/authorized_keys /home/claude/.ssh/authorized_keys | |
| chown -R claude:claude /home/claude/.ssh | |
| chmod 700 /home/claude/.ssh | |
| chmod 600 /home/claude/.ssh/authorized_keys | |
| ok "Chiavi SSH copiate da root" | |
| fi | |
| # installa skills per l'utente claude | |
| CLAUDE_HOME="/home/claude" | |
| mkdir -p "$CLAUDE_HOME/.claude/skills" "$CLAUDE_HOME/.claude/agents" "$CLAUDE_HOME/.claude/rules" | |
| rsync -a --delete "${RSYNC_EXCLUDES[@]}" "$REPO_DIR/" "$CLAUDE_HOME/.claude/skills/" | |
| # cleanup anche per utente claude | |
| for old_dir in warmup sogna coherence person-decode skills agents hooks rules memory sessions scripts config; do | |
| rm -rf "$CLAUDE_HOME/.claude/skills/$old_dir" | |
| done | |
| [ -d "$HOME/.claude/agents" ] && rsync -a "$HOME/.claude/agents/" "$CLAUDE_HOME/.claude/agents/" | |
| [ -d "$HOME/.claude/rules" ] && rsync -a "$HOME/.claude/rules/" "$CLAUDE_HOME/.claude/rules/" | |
| [ -f "$HOME/.claude/CLAUDE.md" ] && cp "$HOME/.claude/CLAUDE.md" "$CLAUDE_HOME/.claude/CLAUDE.md" | |
| [ -f "$HOME/.claude/settings.json" ] && cp "$HOME/.claude/settings.json" "$CLAUDE_HOME/.claude/settings.json" | |
| # hooks per utente claude | |
| if [ -d "$HOME/.claude/hooks" ]; then | |
| mkdir -p "$CLAUDE_HOME/.claude/hooks" | |
| cp "$HOME/.claude/hooks/"*.sh "$CLAUDE_HOME/.claude/hooks/" 2>/dev/null | |
| chmod +x "$CLAUDE_HOME/.claude/hooks/"*.sh 2>/dev/null | |
| fi | |
| # memory per l'utente claude | |
| CLAUDE_MEMORY_DIR="$CLAUDE_HOME/.claude/projects/$(echo "$CLAUDE_HOME" | sed 's|/|-|g')/memory" | |
| mkdir -p "$CLAUDE_MEMORY_DIR" | |
| [ -d "$REPO_DIR/memory" ] && rsync -a "$REPO_DIR/memory/" "$CLAUDE_MEMORY_DIR/" | |
| chown -R claude:claude "$CLAUDE_HOME/.claude" | |
| # GOG_KEYRING_PASSWORD per l'utente claude | |
| grep -q GOG_KEYRING_PASSWORD "$CLAUDE_HOME/.bashrc" 2>/dev/null || echo 'export GOG_KEYRING_PASSWORD=clouded' >> "$CLAUDE_HOME/.bashrc" | |
| warn "Connettiti con: ssh claude@${HOSTNAME_SHORT}" | |
| fi | |
| # ── done ────────────────────────────────────────────────────────────────────── | |
| echo "" | |
| echo -e "${BOLD}${GREEN}Setup completato!${NC}" | |
| echo "" | |
| echo -e " Macchina: ${YELLOW}${HOSTNAME_SHORT}${NC}" | |
| echo -e " Comando: ${BOLD}${CMD_NAME}${NC} (tmux + Telegram)" | |
| echo -e " Skills: $SKILLS_DIR" | |
| echo -e " Memory: $MEMORY_DIR" | |
| echo -e " Agents: $HOME/.claude/agents/" | |
| echo -e " Rules: $HOME/.claude/rules/" | |
| echo -e " CLAUDE.md: $HOME/.claude/CLAUDE.md" | |
| echo -e " Calendar: ms-365-mcp-server (globale)" | |
| echo -e " Gmail: gog CLI (mf@matteoflora.com)" | |
| echo -e " Python: yaml, markdown, bs4, docx" | |
| echo -e " PDF: poppler-utils (pdftotext)" | |
| echo "" | |
| echo -e "Prossimo passo: ${BOLD}${CMD_NAME}${NC} poi ${BOLD}/self dream --force${NC} per consolidare le sessioni" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment