#!/usr/bin/env bash # Install brain artifacts to target (project consumer or user-level ~/.claude) # # Usage: # install.sh --target= --version= # [--dry-run] [--force] # [--with-plugins=yes|no] [--with-mcp=yes|no] [--skip-secrets] set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" source "$SCRIPT_DIR/lib/common.sh" BRAIN_ROOT="${BRAIN_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}" target="" version="" dry_run=0 force=0 with_plugins="default" with_mcp="yes" skip_secrets=0 while [ $# -gt 0 ]; do case "$1" in --target=*) target="${1#--target=}" ;; --version=*) version="${1#--version=}" ;; --dry-run) dry_run=1 ;; --force) force=1 ;; --with-plugins=*) with_plugins="${1#--with-plugins=}" ;; --with-mcp=*) with_mcp="${1#--with-mcp=}" ;; --skip-secrets) skip_secrets=1 ;; *) log_error "Unknown arg: $1"; exit 1 ;; esac shift done [ -n "$target" ] || { log_error "--target required"; exit 1; } [ -n "$version" ] || { log_error "--version required"; exit 1; } require_cmd jq || exit 1 require_cmd git || exit 1 # Detect mode mode="" if [ "$target" = "$HOME/.claude" ] || [ -d "$target/hooks" ] || [ -f "$target/settings.json" ]; then mode="user" elif [ -d "$target/.git" ] || [ -d "$target/docs" ]; then mode="project" else log_error "Cannot detect mode for: $target (expected ~/.claude/ or project dir with docs/)" exit 4 fi log_info "Detected mode: $mode" # Default --with-plugins per mode if [ "$with_plugins" = "default" ]; then [ "$mode" = "user" ] && with_plugins="yes" || with_plugins="no" fi # Checkout version (skip if BRAIN_ROOT override — tests) if [ -z "${BRAIN_ROOT_OVERRIDE:-}" ] && [ -d "$BRAIN_ROOT/.git" ]; then cd "$BRAIN_ROOT" git tag | grep -q "^$version$" || { log_error "Tag $version not found"; exit 3; } git diff --quiet || { log_error "Brain repo dirty"; exit 2; } fi if [ "$dry_run" -eq 1 ]; then log_info "DRY RUN — would install $version to $target ($mode mode)" exit 0 fi # Backup (Bug 4 fix): create backup dir AND actually copy existing target tree # into it BEFORE any modifications, so overwrites are recoverable. if [ "$force" -eq 0 ]; then backup=$(make_backup_dir "$target") log_info "Backup: $backup" # Copy entire target tree into backup, excluding .git/ and .brain-backup-* # rsync preferred; cp -r fallback for environments without rsync. if command -v rsync >/dev/null 2>&1; then rsync -a \ --exclude '.git/' \ --exclude '.brain-backup-*' \ "$target/" "$backup/" 2>/dev/null || true else # cp -r fallback: copy every top-level entry except .git and .brain-backup-* for entry in "$target"/* "$target"/.[!.]* "$target"/..?*; do [ -e "$entry" ] || continue base=$(basename "$entry") case "$base" in .git|.brain-backup-*) continue ;; esac cp -r "$entry" "$backup/" 2>/dev/null || true done fi fi # Copy files per mode if [ "$mode" = "project" ]; then src="$BRAIN_ROOT/project-files" if [ -d "$src" ]; then # Copy all files; strip .template suffix (BOTH middle and trailing — Bug 2 fix) while IFS= read -r -d '' f; do rel="${f#$src/}" # Bug 5: skip brain-internal README (it describes brain repo # structure, not consumer content); consumer's own README stays. case "$rel" in README.md) continue ;; esac # Bug 2: strip both middle '.template.' and trailing '.template' dst_rel="${rel//.template\./.}" dst_rel="${dst_rel%.template}" dst="$target/$dst_rel" mkdir -p "$(dirname "$dst")" cp "$f" "$dst" done < <(find "$src" -type f -print0) fi # Special: .mcp.json from template (если ещё нет) if [ -f "$src/.mcp.json.template" ]; then if [ -f "$target/.mcp.json" ] && [ "$with_mcp" = "yes" ]; then bash "$SCRIPT_DIR/lib/merge-mcp.sh" --mode=project "$target/.mcp.json" "$src/.mcp.json.template" elif [ ! -f "$target/.mcp.json" ] && [ "$with_mcp" = "yes" ]; then cp "$src/.mcp.json.template" "$target/.mcp.json" fi fi elif [ "$mode" = "user" ]; then src="$BRAIN_ROOT/user-level-files" # Hooks [ -d "$src/hooks" ] && mkdir -p "$target/hooks" && cp "$src/hooks"/*.py "$target/hooks/" 2>/dev/null || true # Bug 5: skip brain-internal README in user mode too (we never copy it # explicitly above, but state it defensively for any future bulk copy). # No-op currently — hooks are copied individually, README.md is not. # settings.json merge if [ -f "$src/settings-fragment.json" ]; then bash "$SCRIPT_DIR/lib/merge-settings.sh" "$target/settings.json" "$src/settings-fragment.json" fi # MCP merge (user-level: ~/.claude.json) if [ "$with_mcp" = "yes" ] && [ -f "$src/mcp-user.template.json" ]; then claude_json="$(dirname "$target")/.claude.json" bash "$SCRIPT_DIR/lib/merge-mcp.sh" --mode=user "$claude_json" "$src/mcp-user.template.json" if [ "$skip_secrets" -eq 0 ]; then bash "$SCRIPT_DIR/lib/setup-secrets.sh" "$claude_json" || log_warn "Some secrets unresolved" fi fi # Plugins if [ "$with_plugins" = "yes" ]; then if [ -f "$src/marketplaces.json" ] && [ -f "$src/plugins-manifest.json" ]; then bash "$SCRIPT_DIR/lib/install-plugins.sh" \ --marketplaces="$src/marketplaces.json" \ --manifest="$src/plugins-manifest.json" || exit 6 fi fi fi # Write .brain-version brain_sha="unknown" if [ -d "$BRAIN_ROOT/.git" ]; then brain_sha=$(cd "$BRAIN_ROOT" && git rev-parse "$version" 2>/dev/null || echo "unknown") fi { echo "$version" echo "sha: $brain_sha" } > "$target/.brain-version" # Run verify BRAIN_ROOT="$BRAIN_ROOT" bash "$SCRIPT_DIR/verify.sh" --target="$target" || { log_error "Verification failed after install" exit 5 } log_info "Install complete: $version → $target ($mode mode)"