Skip to content

Commit f9b088f

Browse files
authored
Merge pull request #1165 from obra/mirror-codex-plugin-tooling
Mirror codex plugin tooling
2 parents a569527 + bc25777 commit f9b088f

1 file changed

Lines changed: 382 additions & 0 deletions

File tree

scripts/sync-to-codex-plugin.sh

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
#!/usr/bin/env bash
2+
#
3+
# sync-to-codex-plugin.sh
4+
#
5+
# Sync this superpowers checkout → prime-radiant-inc/openai-codex-plugins.
6+
# Clones the fork fresh into a temp dir, rsyncs upstream content, regenerates
7+
# the Codex overlay file (.codex-plugin/plugin.json) inline, commits, pushes a
8+
# sync branch, and opens a PR.
9+
# Path/user agnostic — auto-detects upstream from script location.
10+
#
11+
# Deterministic: running twice against the same upstream SHA produces PRs with
12+
# identical diffs, so two back-to-back runs can verify the tool itself.
13+
#
14+
# Usage:
15+
# ./scripts/sync-to-codex-plugin.sh # full run
16+
# ./scripts/sync-to-codex-plugin.sh -n # dry run
17+
# ./scripts/sync-to-codex-plugin.sh -y # skip confirm
18+
# ./scripts/sync-to-codex-plugin.sh --local PATH # existing checkout
19+
# ./scripts/sync-to-codex-plugin.sh --base BRANCH # default: main
20+
# ./scripts/sync-to-codex-plugin.sh --bootstrap --assets-src DIR # create initial plugin
21+
#
22+
# Bootstrap mode: skips the "plugin must exist on base" check and seeds
23+
# plugins/superpowers/assets/ from --assets-src <dir> which must contain
24+
# PrimeRadiant_Favicon.svg and PrimeRadiant_Favicon.png. Run once by one
25+
# team member to create the initial PR; every subsequent run is a normal
26+
# (non-bootstrap) sync.
27+
#
28+
# Requires: bash, rsync, git, gh (authenticated), python3.
29+
30+
set -euo pipefail
31+
32+
# =============================================================================
33+
# Config — edit as upstream or canonical plugin shape evolves
34+
# =============================================================================
35+
36+
FORK="prime-radiant-inc/openai-codex-plugins"
37+
DEFAULT_BASE="main"
38+
DEST_REL="plugins/superpowers"
39+
40+
# Paths in upstream that should NOT land in the embedded plugin.
41+
# The Codex-only paths are here too — they're managed by generate/bootstrap
42+
# steps, not by rsync.
43+
#
44+
# All patterns use a leading "/" to anchor them to the source root.
45+
# Unanchored patterns like "scripts/" would match any directory named
46+
# "scripts" at any depth — including legitimate nested dirs like
47+
# skills/brainstorming/scripts/. Anchoring prevents that.
48+
# (.DS_Store is intentionally unanchored — Finder creates them everywhere.)
49+
EXCLUDES=(
50+
# Dotfiles and infra — top-level only
51+
"/.claude/"
52+
"/.claude-plugin/"
53+
"/.codex/"
54+
"/.cursor-plugin/"
55+
"/.git/"
56+
"/.gitattributes"
57+
"/.github/"
58+
"/.gitignore"
59+
"/.opencode/"
60+
"/.version-bump.json"
61+
"/.worktrees/"
62+
".DS_Store"
63+
64+
# Root ceremony files
65+
"/AGENTS.md"
66+
"/CHANGELOG.md"
67+
"/CLAUDE.md"
68+
"/GEMINI.md"
69+
"/RELEASE-NOTES.md"
70+
"/gemini-extension.json"
71+
"/package.json"
72+
73+
# Directories not shipped by canonical Codex plugins
74+
"/commands/"
75+
"/docs/"
76+
"/hooks/"
77+
"/lib/"
78+
"/scripts/"
79+
"/tests/"
80+
"/tmp/"
81+
82+
# Codex-only paths — managed outside rsync
83+
"/.codex-plugin/"
84+
"/assets/"
85+
)
86+
87+
# =============================================================================
88+
# Generated overlay file
89+
# =============================================================================
90+
91+
# Writes the Codex plugin manifest to "$1" with the given upstream version.
92+
# Args: dest_path, version
93+
generate_plugin_json() {
94+
local dest="$1"
95+
local version="$2"
96+
mkdir -p "$(dirname "$dest")"
97+
cat > "$dest" <<EOF
98+
{
99+
"name": "superpowers",
100+
"version": "$version",
101+
"description": "Core skills library for Codex: planning, TDD, debugging, and collaboration workflows.",
102+
"author": {
103+
"name": "Jesse Vincent",
104+
"email": "jesse@fsck.com",
105+
"url": "https://github.com/obra"
106+
},
107+
"homepage": "https://github.com/obra/superpowers",
108+
"repository": "https://github.com/obra/superpowers",
109+
"license": "MIT",
110+
"keywords": [
111+
"skills",
112+
"planning",
113+
"tdd",
114+
"debugging",
115+
"code-review",
116+
"workflow"
117+
],
118+
"skills": "./skills/",
119+
"interface": {
120+
"displayName": "Superpowers",
121+
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
122+
"longDescription": "Use Superpowers to guide agent work through brainstorming, implementation planning, test-driven development, systematic debugging, parallel execution, code review, and finish-the-branch workflows adapted for Codex.",
123+
"developerName": "Jesse Vincent",
124+
"category": "Coding",
125+
"capabilities": [
126+
"Interactive",
127+
"Read",
128+
"Write"
129+
],
130+
"brandColor": "#F59E0B",
131+
"composerIcon": "./assets/superpowers-small.svg",
132+
"logo": "./assets/app-icon.png",
133+
"screenshots": []
134+
}
135+
}
136+
EOF
137+
}
138+
139+
# =============================================================================
140+
# Args
141+
# =============================================================================
142+
143+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
144+
UPSTREAM="$(cd "$SCRIPT_DIR/.." && pwd)"
145+
BASE="$DEFAULT_BASE"
146+
DRY_RUN=0
147+
YES=0
148+
LOCAL_CHECKOUT=""
149+
BOOTSTRAP=0
150+
ASSETS_SRC=""
151+
152+
usage() {
153+
sed -n 's/^# \{0,1\}//;2,27p' "$0"
154+
exit "${1:-0}"
155+
}
156+
157+
while [[ $# -gt 0 ]]; do
158+
case "$1" in
159+
-n|--dry-run) DRY_RUN=1; shift ;;
160+
-y|--yes) YES=1; shift ;;
161+
--local) LOCAL_CHECKOUT="$2"; shift 2 ;;
162+
--base) BASE="$2"; shift 2 ;;
163+
--bootstrap) BOOTSTRAP=1; shift ;;
164+
--assets-src) ASSETS_SRC="$2"; shift 2 ;;
165+
-h|--help) usage 0 ;;
166+
*) echo "Unknown arg: $1" >&2; usage 2 ;;
167+
esac
168+
done
169+
170+
# =============================================================================
171+
# Preflight
172+
# =============================================================================
173+
174+
die() { echo "ERROR: $*" >&2; exit 1; }
175+
176+
command -v rsync >/dev/null || die "rsync not found in PATH"
177+
command -v git >/dev/null || die "git not found in PATH"
178+
command -v gh >/dev/null || die "gh not found — install GitHub CLI"
179+
command -v python3 >/dev/null || die "python3 not found in PATH"
180+
181+
gh auth status >/dev/null 2>&1 || die "gh not authenticated — run 'gh auth login'"
182+
183+
[[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout"
184+
[[ -f "$UPSTREAM/package.json" ]] || die "upstream has no package.json — cannot read version"
185+
186+
# Bootstrap-mode validation
187+
if [[ $BOOTSTRAP -eq 1 ]]; then
188+
[[ -n "$ASSETS_SRC" ]] || die "--bootstrap requires --assets-src <path>"
189+
ASSETS_SRC="$(cd "$ASSETS_SRC" 2>/dev/null && pwd)" || die "assets source '$ASSETS_SRC' is not a directory"
190+
[[ -f "$ASSETS_SRC/PrimeRadiant_Favicon.svg" ]] || die "assets source missing PrimeRadiant_Favicon.svg"
191+
[[ -f "$ASSETS_SRC/PrimeRadiant_Favicon.png" ]] || die "assets source missing PrimeRadiant_Favicon.png"
192+
fi
193+
194+
# Read the upstream version from package.json
195+
UPSTREAM_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$UPSTREAM/package.json")"
196+
[[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from upstream package.json"
197+
198+
UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)"
199+
UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)"
200+
UPSTREAM_SHORT="$(cd "$UPSTREAM" && git rev-parse --short HEAD)"
201+
202+
confirm() {
203+
[[ $YES -eq 1 ]] && return 0
204+
read -rp "$1 [y/N] " ans
205+
[[ "$ans" == "y" || "$ans" == "Y" ]]
206+
}
207+
208+
if [[ "$UPSTREAM_BRANCH" != "main" ]]; then
209+
echo "WARNING: upstream is on '$UPSTREAM_BRANCH', not 'main'"
210+
confirm "Sync from '$UPSTREAM_BRANCH' anyway?" || exit 1
211+
fi
212+
213+
UPSTREAM_STATUS="$(cd "$UPSTREAM" && git status --porcelain)"
214+
if [[ -n "$UPSTREAM_STATUS" ]]; then
215+
echo "WARNING: upstream has uncommitted changes:"
216+
echo "$UPSTREAM_STATUS" | sed 's/^/ /'
217+
echo "Sync will use working-tree state, not HEAD ($UPSTREAM_SHORT)."
218+
confirm "Continue anyway?" || exit 1
219+
fi
220+
221+
# =============================================================================
222+
# Prepare destination (clone fork fresh, or use --local)
223+
# =============================================================================
224+
225+
CLEANUP_DIR=""
226+
cleanup() {
227+
[[ -n "$CLEANUP_DIR" ]] && rm -rf "$CLEANUP_DIR"
228+
}
229+
trap cleanup EXIT
230+
231+
if [[ -n "$LOCAL_CHECKOUT" ]]; then
232+
DEST_REPO="$(cd "$LOCAL_CHECKOUT" && pwd)"
233+
[[ -d "$DEST_REPO/.git" ]] || die "--local path '$DEST_REPO' is not a git checkout"
234+
else
235+
echo "Cloning $FORK..."
236+
CLEANUP_DIR="$(mktemp -d)"
237+
DEST_REPO="$CLEANUP_DIR/openai-codex-plugins"
238+
gh repo clone "$FORK" "$DEST_REPO" >/dev/null
239+
fi
240+
241+
DEST="$DEST_REPO/$DEST_REL"
242+
243+
# Checkout base branch
244+
cd "$DEST_REPO"
245+
git checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
246+
247+
# Plugin-existence check depends on mode
248+
if [[ $BOOTSTRAP -eq 1 ]]; then
249+
[[ ! -d "$DEST" ]] || die "--bootstrap but base branch '$BASE' already has '$DEST_REL/' — use normal sync instead"
250+
mkdir -p "$DEST"
251+
else
252+
[[ -d "$DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap + --assets-src, or pass --base <branch>"
253+
fi
254+
255+
# =============================================================================
256+
# Create sync branch
257+
# =============================================================================
258+
259+
TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)"
260+
if [[ $BOOTSTRAP -eq 1 ]]; then
261+
SYNC_BRANCH="bootstrap/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
262+
else
263+
SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
264+
fi
265+
git checkout -q -b "$SYNC_BRANCH"
266+
267+
# =============================================================================
268+
# Build rsync args
269+
# =============================================================================
270+
271+
RSYNC_ARGS=(-av --delete)
272+
for pat in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$pat"); done
273+
274+
# =============================================================================
275+
# Dry run preview (always shown)
276+
# =============================================================================
277+
278+
echo ""
279+
echo "Upstream: $UPSTREAM ($UPSTREAM_BRANCH @ $UPSTREAM_SHORT)"
280+
echo "Version: $UPSTREAM_VERSION"
281+
echo "Fork: $FORK"
282+
echo "Base: $BASE"
283+
echo "Branch: $SYNC_BRANCH"
284+
if [[ $BOOTSTRAP -eq 1 ]]; then
285+
echo "Mode: BOOTSTRAP (creating initial plugin from scratch)"
286+
echo "Assets: $ASSETS_SRC"
287+
fi
288+
echo ""
289+
echo "=== Preview (rsync --dry-run) ==="
290+
rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$DEST/"
291+
echo "=== End preview ==="
292+
echo ""
293+
echo "Overlay file (.codex-plugin/plugin.json) will be regenerated with"
294+
echo "version $UPSTREAM_VERSION regardless of rsync output."
295+
if [[ $BOOTSTRAP -eq 1 ]]; then
296+
echo "Assets (superpowers-small.svg, app-icon.png) will be seeded from:"
297+
echo " $ASSETS_SRC"
298+
fi
299+
300+
if [[ $DRY_RUN -eq 1 ]]; then
301+
echo ""
302+
echo "Dry run only. Nothing was changed or pushed."
303+
exit 0
304+
fi
305+
306+
# =============================================================================
307+
# Apply
308+
# =============================================================================
309+
310+
echo ""
311+
confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; }
312+
313+
echo ""
314+
echo "Syncing upstream content..."
315+
rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/"
316+
317+
if [[ $BOOTSTRAP -eq 1 ]]; then
318+
echo "Seeding brand assets..."
319+
mkdir -p "$DEST/assets"
320+
cp "$ASSETS_SRC/PrimeRadiant_Favicon.svg" "$DEST/assets/superpowers-small.svg"
321+
cp "$ASSETS_SRC/PrimeRadiant_Favicon.png" "$DEST/assets/app-icon.png"
322+
fi
323+
324+
echo "Regenerating overlay file..."
325+
generate_plugin_json "$DEST/.codex-plugin/plugin.json" "$UPSTREAM_VERSION"
326+
327+
# Bail early if nothing actually changed
328+
cd "$DEST_REPO"
329+
if [[ -z "$(git status --porcelain "$DEST_REL")" ]]; then
330+
echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)."
331+
exit 0
332+
fi
333+
334+
# =============================================================================
335+
# Commit, push, open PR
336+
# =============================================================================
337+
338+
git add "$DEST_REL"
339+
340+
if [[ $BOOTSTRAP -eq 1 ]]; then
341+
COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
342+
PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
343+
344+
Creates \`plugins/superpowers/\` from scratch: upstream content via rsync, \`.codex-plugin/plugin.json\` regenerated inline, brand assets seeded from a local Brand Assets directory.
345+
346+
Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap --assets-src <path>\`
347+
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
348+
349+
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs and will not touch the \`assets/\` directory."
350+
else
351+
COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
352+
PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
353+
354+
Run via: \`scripts/sync-to-codex-plugin.sh\`
355+
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
356+
357+
Running the sync tool again against the same upstream SHA should produce a PR with an identical diff — use that to verify the tool is behaving."
358+
fi
359+
360+
git commit --quiet -m "$COMMIT_TITLE
361+
362+
Automated sync via scripts/sync-to-codex-plugin.sh
363+
Upstream: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
364+
Branch: $SYNC_BRANCH"
365+
366+
echo "Pushing $SYNC_BRANCH to $FORK..."
367+
git push -u origin "$SYNC_BRANCH" --quiet
368+
369+
echo "Opening PR..."
370+
PR_URL="$(gh pr create \
371+
--repo "$FORK" \
372+
--base "$BASE" \
373+
--head "$SYNC_BRANCH" \
374+
--title "$COMMIT_TITLE" \
375+
--body "$PR_BODY")"
376+
377+
PR_NUM="${PR_URL##*/}"
378+
DIFF_URL="https://github.com/$FORK/pull/$PR_NUM/files"
379+
380+
echo ""
381+
echo "PR opened: $PR_URL"
382+
echo "Diff view: $DIFF_URL"

0 commit comments

Comments
 (0)