Last active
March 10, 2026 21:45
-
-
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 Claude Code su una macchina nuova | |
| # Usage: curl -fsSL https://raw.githubusercontent.com/lastknight/claude-skills/main/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" | |
| HOSTNAME_SHORT="$(hostname -s 2>/dev/null || hostname)" | |
| CMD_NAME="clouded" | |
| # 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 | |
| echo -e "\n${BOLD}clouded bootstrap${NC} — macchina: ${YELLOW}${HOSTNAME_SHORT}${NC}\n" | |
| # ── 1. prerequisiti ─────────────────────────────────────────────────────────── | |
| log "Controllo prerequisiti..." | |
| check_cmd() { | |
| command -v "$1" &>/dev/null && return 0 | |
| return 1 | |
| } | |
| # 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 | |
| apt-get install -y gh 2>/dev/null || \ | |
| (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 && \ | |
| 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 | |
| if ! check_cmd rsync; then | |
| warn "rsync non trovato. Provo ad installarlo..." | |
| if check_cmd brew; then brew install rsync | |
| elif check_cmd apt-get; then apt-get install -y rsync | |
| else err "Installa rsync manualmente." | |
| fi | |
| fi | |
| ok "rsync" | |
| # git | |
| check_cmd git || err "git non trovato. Installalo prima." | |
| ok "git" | |
| # tmux | |
| if ! check_cmd tmux; then | |
| warn "tmux non trovato. Installo..." | |
| if check_cmd brew; then brew install tmux | |
| elif check_cmd apt-get; then apt-get install -y tmux | |
| else warn "Installa tmux manualmente." | |
| fi | |
| fi | |
| check_cmd tmux && ok "tmux $(tmux -V 2>/dev/null | awk '{print $2}')" || true | |
| # ── 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 $HOME/.claude-skills-repo 2>/dev/null || true | |
| if [ -d $HOME/.claude-skills-repo/.git ] && [ "$(stat -c '%u' $HOME/.claude-skills-repo)" = "$(id -u)" ]; then | |
| cd $HOME/.claude-skills-repo && git pull --quiet | |
| else | |
| rm -rf $HOME/.claude-skills-repo | |
| gh repo clone "$REPO" $HOME/.claude-skills-repo -- --quiet | |
| fi | |
| mkdir -p "$SKILLS_DIR" | |
| # salva stile attivo se esiste | |
| ACTIVE_STYLE="" | |
| if [ -L "$SKILLS_DIR/ACTIVE_STYLE.md" ]; then | |
| ACTIVE_STYLE="$(readlink "$SKILLS_DIR/ACTIVE_STYLE.md")" | |
| fi | |
| rsync -a --delete \ | |
| --exclude='.git' \ | |
| --exclude='README.md' \ | |
| --exclude='.DS_Store' \ | |
| --exclude='clouded-bootstrap.sh' \ | |
| $HOME/.claude-skills-repo/ "$SKILLS_DIR/" | |
| # 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 "Skills sincronizzate in $SKILLS_DIR" | |
| # ── 3b. memory ──────────────────────────────────────────────────────────────── | |
| log "Sincronizzazione memorie..." | |
| MEMORY_DIR="$HOME/.claude/projects/$(echo "$HOME" | sed 's|/|-|g')/memory" | |
| mkdir -p "$MEMORY_DIR" | |
| if [ -d $HOME/.claude-skills-repo/memory ]; then | |
| rsync -a $HOME/.claude-skills-repo/memory/ "$MEMORY_DIR/" | |
| ok "Memorie sincronizzate in $MEMORY_DIR" | |
| else | |
| warn "Nessuna directory memory nel repo — skip" | |
| 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 | |
| SESSION="clouded" | |
| if command -v tmux &>/dev/null; then | |
| if tmux has-session -t "$SESSION" 2>/dev/null; then | |
| exec tmux attach-session -t "$SESSION" | |
| else | |
| # crea sessione persistente con loop di riavvio automatico | |
| tmux new-session -d -s "$SESSION" "while true; do claude --dangerously-skip-permissions; echo 'Claude uscito. Riavvio in 2s... (Ctrl+C per fermare)'; sleep 2; done" | |
| exec tmux attach-session -t "$SESSION" | |
| fi | |
| else | |
| exec claude --dangerously-skip-permissions "$@" | |
| fi | |
| ENDOFSCRIPT | |
| chmod +x "$CMD_PATH" | |
| ok "Comando installato: $CMD_PATH" | |
| # ── 5. PATH check ───────────────────────────────────────────────────────────── | |
| if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then | |
| warn "$INSTALL_DIR non è nel PATH. Aggiungi al tuo shell profile:" | |
| echo "" | |
| echo " export PATH=\"$INSTALL_DIR:\$PATH\"" | |
| echo "" | |
| SHELL_RC="" | |
| case "${SHELL:-}" in | |
| */zsh) SHELL_RC="$HOME/.zshrc" ;; | |
| */bash) SHELL_RC="$HOME/.bashrc" ;; | |
| esac | |
| if [ -n "$SHELL_RC" ]; then | |
| read -r -p "Aggiungere automaticamente a $SHELL_RC? [y/N] " choice | |
| if [[ "$choice" =~ ^[Yy]$ ]]; then | |
| echo "" >> "$SHELL_RC" | |
| echo "# clouded" >> "$SHELL_RC" | |
| echo "export PATH=\"$INSTALL_DIR:\$PATH\"" >> "$SHELL_RC" | |
| ok "Aggiunto a $SHELL_RC — esegui: source $SHELL_RC" | |
| fi | |
| fi | |
| fi | |
| # ── 6. MCP: Microsoft 365 Calendar ──────────────────────────────────────────── | |
| echo "" | |
| log "Configurazione MCP Microsoft 365..." | |
| # registra il server MCP in Claude Code (idempotente) | |
| if claude mcp list 2>/dev/null | grep -q "ms365"; then | |
| ok "MCP ms365 già configurato" | |
| else | |
| claude mcp add ms365 -- npx -y @softeria/ms-365-mcp-server --org-mode | |
| ok "MCP ms365 aggiunto" | |
| fi | |
| # autenticazione Microsoft 365 (solo se c'è un browser) | |
| echo "" | |
| if $HAS_DISPLAY; then | |
| read -r -p "Autenticarsi con Microsoft 365 ora? (richiede browser) [y/N] " choice | |
| if [[ "$choice" =~ ^[Yy]$ ]]; then | |
| log "Avvio autenticazione Microsoft 365..." | |
| echo -e "${YELLOW}Segui le istruzioni nel browser. Ctrl+C per saltare.${NC}\n" | |
| npx -y @softeria/ms-365-mcp-server --login --org-mode && ok "Microsoft 365 autenticato" || warn "Autenticazione saltata. Esegui manualmente: npx @softeria/ms-365-mcp-server --login --org-mode" | |
| else | |
| warn "Autenticazione saltata. Quando vuoi: npx @softeria/ms-365-mcp-server --login --org-mode" | |
| fi | |
| else | |
| warn "Server headless: autenticazione Microsoft 365 da fare su macchina con browser" | |
| fi | |
| # ── 7. Gmail: gog CLI ───────────────────────────────────────────────────────── | |
| echo "" | |
| log "Configurazione Gmail (gog)..." | |
| # installa gog se mancante | |
| 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" | |
| curl -fsSL "$GOG_URL" | tar -xz -C /usr/local/bin gog | |
| chmod +x /usr/local/bin/gog | |
| fi | |
| fi | |
| check_cmd gog && ok "gog $(gog --version 2>/dev/null | head -1)" || warn "gog non installato — Gmail non disponibile" | |
| # registra le credenziali OAuth e token (dal repo, solo se gog è disponibile) | |
| if check_cmd gog; then | |
| GOG_CREDS="$HOME/.claude-skills-repo/config/gog-credentials.json" | |
| GOG_TOKEN="$HOME/.claude-skills-repo/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 | |
| # configura keyring file (funziona su server headless) | |
| 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" | |
| # aggiungi la password al bashrc per sessioni future | |
| grep -q GOG_KEYRING_PASSWORD "$HOME/.bashrc" 2>/dev/null || echo 'export GOG_KEYRING_PASSWORD=clouded' >> "$HOME/.bashrc" | |
| else | |
| # fallback: browser se disponibile | |
| 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 | |
| # ── 8. utente 'claude' per SSH (solo se root) ───────────────────────────────── | |
| if [ "$(id -u)" = "0" ]; then | |
| echo "" | |
| log "Creazione utente 'claude' per SSH con --dangerously-skip-permissions..." | |
| 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 clouded per l'utente claude | |
| CLAUDE_HOME="/home/claude" | |
| mkdir -p "$CLAUDE_HOME/.claude/skills" | |
| rsync -a "$SKILLS_DIR/" "$CLAUDE_HOME/.claude/skills/" | |
| chown -R claude:claude "$CLAUDE_HOME/.claude" | |
| # clouded è già installato in /usr/local/bin con tmux wrapper | |
| 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}" | |
| echo -e " Skills: $SKILLS_DIR" | |
| echo -e " Memory: $MEMORY_DIR" | |
| echo -e " MCP: ms365 (Microsoft 365 Calendar)" | |
| echo -e " Gmail: gog CLI (mf@matteoflora.com)" | |
| echo "" | |
| echo -e "Lancia con: ${BOLD}${CMD_NAME}${NC}" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment