Fixed setups
This commit is contained in:
+186
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-shot setup for the git-manager plugin.
|
||||
#
|
||||
# Order matters here, deliberately: the permission rule is established
|
||||
# FIRST, against a stable path we control ourselves
|
||||
# (~/.agent-skills/git-manager/bin/), not against Claude Code's internal
|
||||
# plugin cache path (~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/),
|
||||
# which changes on every plugin update and is why earlier versions of this
|
||||
# setup needed to "locate itself" at all. Configuration (repos, hook) comes
|
||||
# after, and re-running this script after a plugin update re-syncs the
|
||||
# stable copy without ever touching the permission rule again.
|
||||
#
|
||||
# Usage:
|
||||
# setup.sh --settings-scope (project|user) [--project-dir <path>]
|
||||
# [--repo <name>=<path> ...] [--install-hook]
|
||||
#
|
||||
# Prints a JSON summary of what changed to stdout at the end.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SKILL_DIR="$(realpath "$PLUGIN_DIR/skills/git-manager")"
|
||||
SOURCE_HOOK_PATH="$PLUGIN_DIR/hooks/force-ask-on-chaining.sh"
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "Error: python3 is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 1. Establish the stable, self-controlled target location and copy
|
||||
# the wrapper script (+ its dependency) and the hook there. This is the
|
||||
# ONLY thing whitelisted permissions will ever point to — never the
|
||||
# Claude Code plugin cache path, which changes on every version bump. ---
|
||||
STABLE_DIR="$HOME/.agent-skills/git-manager/bin"
|
||||
mkdir -p "$STABLE_DIR"
|
||||
cp "$SKILL_DIR/scripts/git_cmd.sh" "$STABLE_DIR/git_cmd.sh"
|
||||
cp "$SKILL_DIR/scripts/_lib.sh" "$STABLE_DIR/_lib.sh"
|
||||
cp "$SOURCE_HOOK_PATH" "$STABLE_DIR/force-ask-on-chaining.sh"
|
||||
chmod +x "$STABLE_DIR/git_cmd.sh" "$STABLE_DIR/force-ask-on-chaining.sh"
|
||||
|
||||
WRAPPER_PATH="$STABLE_DIR/git_cmd.sh"
|
||||
HOOK_PATH="$STABLE_DIR/force-ask-on-chaining.sh"
|
||||
# Tilde-form paths for permission rules and SKILL.md's example commands —
|
||||
# Claude Code matches Bash permission rules against the literal,
|
||||
# unexpanded command text, so a rule written with the resolved absolute
|
||||
# $HOME path (e.g. /home/hmk/...) does NOT match a command Claude typed as
|
||||
# "~/...", even though both point at the same file. Keep both sides in
|
||||
# tilde form so they actually match.
|
||||
TILDE_WRAPPER_PATH="~/.agent-skills/git-manager/bin/git_cmd.sh"
|
||||
TILDE_HOOK_PATH="~/.agent-skills/git-manager/bin/force-ask-on-chaining.sh"
|
||||
TILDE_CONFIG_FILE="~/.agent-skills/git-manager/config.json"
|
||||
|
||||
# --- 2. Parse arguments ---
|
||||
SETTINGS_SCOPE=""
|
||||
PROJECT_DIR="$PWD"
|
||||
INSTALL_HOOK=0
|
||||
REPOS=()
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--settings-scope) SETTINGS_SCOPE="$2"; shift 2 ;;
|
||||
--project-dir) PROJECT_DIR="$2"; shift 2 ;;
|
||||
--repo) REPOS+=("$2"); shift 2 ;;
|
||||
--install-hook) INSTALL_HOOK=1; shift ;;
|
||||
*) echo "Error: unknown argument '$1'" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$SETTINGS_SCOPE" != "project" ] && [ "$SETTINGS_SCOPE" != "user" ]; then
|
||||
echo "Error: --settings-scope must be 'project' or 'user'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$SETTINGS_SCOPE" = "project" ]; then
|
||||
SETTINGS_FILE="$PROJECT_DIR/.claude/settings.json"
|
||||
else
|
||||
SETTINGS_FILE="$HOME/.claude/settings.json"
|
||||
fi
|
||||
|
||||
CONFIG_DIR="$HOME/.agent-skills/git-manager"
|
||||
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# --- 3. Write permissions FIRST, against the stable path from step 1 —
|
||||
# before touching any repo configuration. This is what makes "permissions
|
||||
# first, then configuration" actually true, per the point of this
|
||||
# redesign: the target path was already fixed and known before we even
|
||||
# parsed arguments. ---
|
||||
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
||||
|
||||
python3 - "$SETTINGS_FILE" "$TILDE_WRAPPER_PATH" "$TILDE_CONFIG_FILE" "$HOOK_PATH" "$INSTALL_HOOK" << 'PYEOF'
|
||||
import json, os, sys
|
||||
|
||||
settings_file, tilde_wrapper_path, tilde_config_file, hook_path, install_hook = sys.argv[1:6]
|
||||
install_hook = install_hook == "1"
|
||||
|
||||
settings = {}
|
||||
if os.path.exists(settings_file):
|
||||
with open(settings_file) as f:
|
||||
settings = json.load(f)
|
||||
|
||||
perms = settings.setdefault("permissions", {})
|
||||
allow = perms.setdefault("allow", [])
|
||||
|
||||
# Tilde form here on purpose: Claude Code matches permission rules against
|
||||
# the literal, unexpanded command text a model produces, which uses "~"
|
||||
# (per SKILL.md), not the resolved absolute $HOME path.
|
||||
new_rules = [
|
||||
f"Bash({tilde_wrapper_path}:*)",
|
||||
f"Read({tilde_config_file})",
|
||||
]
|
||||
for rule in new_rules:
|
||||
if rule not in allow:
|
||||
allow.append(rule)
|
||||
|
||||
if install_hook:
|
||||
hooks = settings.setdefault("hooks", {})
|
||||
pre_tool_use = hooks.setdefault("PreToolUse", [])
|
||||
already_present = any(
|
||||
entry.get("matcher") == "Bash"
|
||||
and any(h.get("command") == hook_path for h in entry.get("hooks", []))
|
||||
for entry in pre_tool_use
|
||||
)
|
||||
if not already_present:
|
||||
pre_tool_use.append({
|
||||
"matcher": "Bash",
|
||||
# Real absolute path here (not tilde) — this is executed
|
||||
# directly by Claude Code as a subprocess, not matched as text,
|
||||
# so it needs to be an actually-invokable path.
|
||||
"hooks": [{"type": "command", "command": hook_path}],
|
||||
})
|
||||
|
||||
with open(settings_file, "w") as f:
|
||||
json.dump(settings, f, indent=2)
|
||||
f.write("\n")
|
||||
PYEOF
|
||||
|
||||
# --- 4. THEN configuration: write/merge repos, if any were given ---
|
||||
REPOS_JSON="[]"
|
||||
if [ "${#REPOS[@]}" -gt 0 ]; then
|
||||
REPOS_JSON=$(python3 -c '
|
||||
import json, sys
|
||||
pairs = sys.argv[1:]
|
||||
repos = []
|
||||
for p in pairs:
|
||||
name, path = p.split("=", 1)
|
||||
repos.append({"name": name, "path": path})
|
||||
print(json.dumps(repos))
|
||||
' "${REPOS[@]}")
|
||||
fi
|
||||
|
||||
python3 - "$CONFIG_FILE" "$REPOS_JSON" << 'PYEOF'
|
||||
import json, os, sys
|
||||
config_file, new_repos_json = sys.argv[1], sys.argv[2]
|
||||
new_repos = json.loads(new_repos_json)
|
||||
|
||||
existing = {"repos": []}
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file) as f:
|
||||
existing = json.load(f)
|
||||
|
||||
existing_by_name = {r["name"]: r for r in existing.get("repos", [])}
|
||||
for r in new_repos:
|
||||
existing_by_name[r["name"]] = r # upsert: new/updated path always wins
|
||||
existing["repos"] = list(existing_by_name.values())
|
||||
|
||||
with open(config_file, "w") as f:
|
||||
json.dump(existing, f, indent=2)
|
||||
f.write("\n")
|
||||
PYEOF
|
||||
|
||||
# --- 5. Summary ---
|
||||
python3 - "$STABLE_DIR" "$SETTINGS_FILE" "$CONFIG_FILE" "$INSTALL_HOOK" "$REPOS_JSON" "$TILDE_WRAPPER_PATH" "$TILDE_CONFIG_FILE" << 'PYEOF'
|
||||
import json, sys
|
||||
stable_dir, settings_file, config_file, install_hook, repos_json, tilde_wrapper_path, tilde_config_file = sys.argv[1:8]
|
||||
print(json.dumps({
|
||||
"stable_script_location": stable_dir,
|
||||
"settings_file": settings_file,
|
||||
"config_file": config_file,
|
||||
"hook_installed": install_hook == "1",
|
||||
"repos_registered": json.loads(repos_json),
|
||||
"permission_rules_added": [f"Bash({tilde_wrapper_path}:*)", f"Read({tilde_config_file})"],
|
||||
"note": "The permission rule points at a stable location this script controls (~/.agent-skills/git-manager/bin/), not at Claude Code's internal plugin cache — so it survives future plugin updates without changing. It's written in tilde form (~/...) to match the literal, unexpanded command text Claude Code matches against, not the resolved absolute $HOME path. Permissions are written to disk now, but Claude Code loads permissions at session start and does not always pick up changes made by its own file edits within the same session — if a git_cmd.sh call still prompts after this, start a fresh session rather than re-running setup."
|
||||
}, indent=2))
|
||||
PYEOF
|
||||
Reference in New Issue
Block a user