|
| 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