Skip to content

Instantly share code, notes, and snippets.

@lastknight
Last active March 10, 2026 21:45
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 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