Repository navigation
Expand file tree
/
Copy pathsync-claude-settings.sh
More file actions
executable file
·152 lines (134 loc) · 5.69 KB
/
Copy pathsync-claude-settings.sh
File metadata and controls
executable file
·152 lines (134 loc) · 5.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/usr/bin/env bash
# Roll the scenerystack Claude Code plugin out to the SceneryStack repos by merging
# the canonical keys from config/claude-settings.json into each repo's
# .claude/settings.json. Mirrors sync-dependabot.sh, but MERGES (never clobbers):
# existing keys in a repo's settings are preserved; only the marketplace entry and
# the scenerystack plugin flag are added/updated.
#
# The canonical block is DERIVED, not stored: the marketplace id comes from
# .claude-plugin/marketplace.json and the org from the catalog, so a rename of
# either is a single edit at the source. config/claude-settings.json is rewritten
# from those values on every run and stays the artifact other tools read.
#
# Targets every repo whose catalog framework is "SceneryStack" (all the sims in
# structure/repos.json + the template — i.e. the CONVENTIONS.md scope). Operates on sibling checkouts in the workspace, like the
# other fleet scripts; it writes files only — committing/pushing (or opening PRs) is left to
# you or to fleet-exec.sh.
#
# scripts/sync-claude-settings.sh --dry-run # show what would change, write nothing
# scripts/sync-claude-settings.sh # merge into each sibling repo
# scripts/sync-claude-settings.sh DopplerEffect # limit to named repo(s)
#
# To fan it out as one PR per repo instead, run the per-repo form under fleet-exec.sh
# (see scripts/README.md).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SOURCE="$REPO_ROOT/config/claude-settings.json"
MARKETPLACE="$REPO_ROOT/.claude-plugin/marketplace.json"
CATALOG="${FLEET_CATALOG:-${OPENPHYSICS_CATALOG:-$REPO_ROOT/structure/repos.json}}"
WORKSPACE="${FLEET_WORKSPACE:-${OPENPHYSICS_WORKSPACE:-$(cd "$REPO_ROOT/.." && pwd)}}"
command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; }
command -v python3 >/dev/null || { echo "python3 is required" >&2; exit 1; }
[ -f "$MARKETPLACE" ] || { echo "missing $MARKETPLACE" >&2; exit 1; }
# Regenerate the canonical block from the marketplace id + the catalog org.
MARKETPLACE_ID="$(jq -r '.name' "$MARKETPLACE")"
ORG="${FLEET_ORG:-${OPENPHYSICS_ORG:-$(jq -r '.organization' "$CATALOG")}}"
jq -n --arg id "$MARKETPLACE_ID" --arg repo "$ORG/Baton" '
{
extraKnownMarketplaces: { ($id): { source: { source: "github", repo: $repo } } },
enabledPlugins: { ("scenerystack@" + $id): true }
}' > "$SOURCE"
[ -f "$SOURCE" ] || { echo "missing $SOURCE" >&2; exit 1; }
DRY_RUN=0
FILTER=()
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
-*) echo "unknown option: $arg" >&2; exit 1 ;;
*) FILTER+=("$arg") ;;
esac
done
# SceneryStack repos = the CONVENTIONS scope (sims + template).
mapfile -t REPOS < <(jq -r '.repos[] | select(.framework == "SceneryStack") | .name' "$CATALOG")
wants() {
[ "${#FILTER[@]}" -eq 0 ] && return 0
local name="$1"
for f in "${FILTER[@]}"; do [ "$f" = "$name" ] && return 0; done
return 1
}
changed=0
missing=0
for repo in "${REPOS[@]}"; do
wants "$repo" || continue
dir="$WORKSPACE/$repo"
if [ ! -d "$dir/.git" ]; then
echo "SKIP $repo (not checked out at $dir)"
missing=$((missing + 1))
continue
fi
target="$dir/.claude/settings.json"
# Deep-merge SOURCE into TARGET (TARGET wins only for keys SOURCE doesn't set);
# for our two keys SOURCE provides the canonical value. Reports whether it changed.
result="$(SOURCE="$SOURCE" TARGET="$target" DRY_RUN="$DRY_RUN" python3 - "$repo" <<'PY'
import json, os, sys
repo = sys.argv[1]
src = json.load(open(os.environ["SOURCE"]))
target = os.environ["TARGET"]
try:
with open(target) as f:
cur = json.load(f)
except FileNotFoundError:
cur = {}
def deep_merge(dst, add):
for k, v in add.items():
if isinstance(v, dict) and isinstance(dst.get(k), dict):
deep_merge(dst[k], v)
else:
dst[k] = v
return dst
before = json.dumps(cur, sort_keys=True)
merged = deep_merge(json.loads(json.dumps(cur)), src)
# Prune superseded entries. A merge alone would leave a renamed marketplace
# (or an org-renamed source repo) sitting beside the canonical one, so drop any
# marketplace that points at the same Baton repo under a different key, and any
# scenerystack plugin flag that is not the current id. Unrelated marketplaces
# and plugins the repo has added are left alone.
canonical_id = next(iter(src["extraKnownMarketplaces"]))
canonical_repo = src["extraKnownMarketplaces"][canonical_id]["source"]["repo"]
canonical_plugin = next(iter(src["enabledPlugins"]))
markets = merged.get("extraKnownMarketplaces", {})
for key in [k for k in markets if k != canonical_id]:
entry = markets[key]
repo = (entry or {}).get("source", {}).get("repo", "")
if repo.split("/")[-1] == canonical_repo.split("/")[-1]:
del markets[key]
plugins = merged.get("enabledPlugins", {})
for key in [k for k in plugins if k != canonical_plugin]:
if key.split("@")[0] == canonical_plugin.split("@")[0]:
del plugins[key]
after = json.dumps(merged, sort_keys=True)
if before == after:
print("OK %s (already up to date)" % repo)
sys.exit(0)
if os.environ.get("DRY_RUN") == "1":
print("WOULD UPDATE %s" % repo)
sys.exit(10)
os.makedirs(os.path.dirname(target), exist_ok=True)
with open(target, "w") as f:
json.dump(merged, f, indent=2)
f.write("\n")
print("UPDATED %s" % repo)
sys.exit(10)
PY
)" && rc=0 || rc=$?
echo "$result"
[ "$rc" -eq 10 ] && changed=$((changed + 1))
done
echo
if [ "$DRY_RUN" -eq 1 ]; then
echo "Dry run: $changed repo(s) would change, $missing not checked out. Re-run without --dry-run to apply."
else
echo "Done: $changed repo(s) updated, $missing not checked out."
echo "Review, then commit/push per repo (or use fleet-exec.sh to open PRs)."
fi