Skip to content

Instantly share code, notes, and snippets.

@lastknight
Last active July 19, 2026 11:59
Show Gist options
  • Select an option

  • Save lastknight/f21271d761a86a5ab2b6a5f2e73256d5 to your computer and use it in GitHub Desktop.

Select an option

Save lastknight/f21271d761a86a5ab2b6a5f2e73256d5 to your computer and use it in GitHub Desktop.
clouded-bootstrap: one-curl Claude Code setup with skills
#!/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