dc71e2338e
Closes тех-долг из routing.md v1.1 §4.1 — install-plugins.sh теперь
фильтрует entries по полю `target` относительно режима install'а.
Added флаг `--install-target=user|consumer` (required, с fallback на 'user'
+ warning для backwards-compat прямых вызовов из тестов). Filter jq-выражение:
user: target ∈ {"user-level", "user-level+consumer"}
consumer: target ∈ {"consumer", "user-level+consumer"}
scripts/install.sh теперь передаёт --install-target автоматически на основе
detected $mode (user|project). Regression: текущие 8 entries → 8 plugins при
--install-target=user, 2 plugins (FD + UPM, target=user-level+consumer) при
--install-target=consumer.
Подготовка под Phase 1: добавление 4 consumer-only entries (target=consumer)
и 1 user-level+consumer entry (context7) в plugins-manifest.json — filter
обеспечит, что в brain user-level попадут только нужные.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
175 lines
6.3 KiB
Bash
175 lines
6.3 KiB
Bash
#!/usr/bin/env bash
|
|
# Install brain artifacts to target (project consumer or user-level ~/.claude)
|
|
#
|
|
# Usage:
|
|
# install.sh --target=<path> --version=<tag>
|
|
# [--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
|
|
# Map mode (user|project) to install-target (user|consumer)
|
|
plugin_target="user"
|
|
[ "$mode" = "project" ] && plugin_target="consumer"
|
|
bash "$SCRIPT_DIR/lib/install-plugins.sh" \
|
|
--marketplaces="$src/marketplaces.json" \
|
|
--manifest="$src/plugins-manifest.json" \
|
|
--install-target="$plugin_target" || 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)"
|