sync-to-codex-plugin.sh 13 KB

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