sync-to-codex-plugin.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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 files (.codex-plugin/plugin.json + agents/openai.yaml)
  8. # inline, commits, pushes a 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 with confirm
  16. # ./scripts/sync-to-codex-plugin.sh -n # dry run, no clone/push/PR
  17. # ./scripts/sync-to-codex-plugin.sh -y # skip confirmation
  18. # ./scripts/sync-to-codex-plugin.sh --local PATH # use existing checkout
  19. # ./scripts/sync-to-codex-plugin.sh --base BRANCH # target branch (default: main)
  20. #
  21. # Requires: bash, rsync, git, gh (authenticated), python3.
  22. set -euo pipefail
  23. # =============================================================================
  24. # Config — edit as upstream or canonical plugin shape evolves
  25. # =============================================================================
  26. FORK="prime-radiant-inc/openai-codex-plugins"
  27. DEFAULT_BASE="main"
  28. DEST_REL="plugins/superpowers"
  29. # Paths in upstream that should NOT land in the embedded plugin.
  30. # Both the Codex-overlay files are here too — they're managed by the
  31. # generate-overlays step, not by rsync.
  32. EXCLUDES=(
  33. # Dotfiles and infra
  34. ".claude/"
  35. ".claude-plugin/"
  36. ".codex/"
  37. ".cursor-plugin/"
  38. ".git/"
  39. ".gitattributes"
  40. ".github/"
  41. ".gitignore"
  42. ".opencode/"
  43. ".version-bump.json"
  44. ".worktrees/"
  45. ".DS_Store"
  46. # Root ceremony files
  47. "AGENTS.md"
  48. "CHANGELOG.md"
  49. "CLAUDE.md"
  50. "CODE_OF_CONDUCT.md"
  51. "GEMINI.md"
  52. "RELEASE-NOTES.md"
  53. "gemini-extension.json"
  54. "package.json"
  55. # Directories not shipped by canonical Codex plugins
  56. "commands/"
  57. "docs/"
  58. "hooks/"
  59. "lib/"
  60. "scripts/"
  61. "tests/"
  62. "tmp/"
  63. # Codex-overlay files — regenerated below, not synced
  64. ".codex-plugin/"
  65. "agents/openai.yaml"
  66. )
  67. # =============================================================================
  68. # Generated overlay files
  69. # =============================================================================
  70. # Writes the Codex plugin manifest to "$1" with the given upstream version.
  71. # Args: dest_path, version
  72. generate_plugin_json() {
  73. local dest="$1"
  74. local version="$2"
  75. mkdir -p "$(dirname "$dest")"
  76. cat > "$dest" <<EOF
  77. {
  78. "name": "superpowers",
  79. "version": "$version",
  80. "description": "Core skills library for Codex: planning, TDD, debugging, and collaboration workflows.",
  81. "author": {
  82. "name": "Jesse Vincent",
  83. "email": "jesse@fsck.com",
  84. "url": "https://github.com/obra"
  85. },
  86. "homepage": "https://github.com/obra/superpowers",
  87. "repository": "https://github.com/obra/superpowers",
  88. "license": "MIT",
  89. "keywords": [
  90. "skills",
  91. "planning",
  92. "tdd",
  93. "debugging",
  94. "code-review",
  95. "workflow"
  96. ],
  97. "skills": "./skills/",
  98. "interface": {
  99. "displayName": "Superpowers",
  100. "shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
  101. "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.",
  102. "developerName": "Jesse Vincent",
  103. "category": "Coding",
  104. "capabilities": [
  105. "Interactive",
  106. "Read",
  107. "Write"
  108. ],
  109. "websiteURL": "https://github.com/obra/superpowers",
  110. "privacyPolicyURL": "https://docs.github.com/site-policy/privacy-policies/github-general-privacy-statement",
  111. "termsOfServiceURL": "https://docs.github.com/en/site-policy/github-terms/github-terms-of-service",
  112. "defaultPrompt": [
  113. "Use Superpowers to plan this feature before we code",
  114. "Debug this bug with a systematic root-cause workflow",
  115. "Turn this approved design into an implementation plan"
  116. ],
  117. "brandColor": "#F59E0B",
  118. "screenshots": []
  119. }
  120. }
  121. EOF
  122. }
  123. # Writes the plugin-level agents/openai.yaml to "$1".
  124. # Args: dest_path
  125. generate_agents_openai_yaml() {
  126. local dest="$1"
  127. mkdir -p "$(dirname "$dest")"
  128. cat > "$dest" <<'EOF'
  129. interface:
  130. display_name: "Superpowers"
  131. short_description: "Planning, TDD, debugging, and delivery workflows for coding agents"
  132. default_prompt: "Use Superpowers to brainstorm a design, write an implementation plan, run test-driven development, debug bugs systematically, or finish and ship a development branch."
  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. usage() {
  145. sed -n 's/^# \{0,1\}//;2,20p' "$0"
  146. exit "${1:-0}"
  147. }
  148. while [[ $# -gt 0 ]]; do
  149. case "$1" in
  150. -n|--dry-run) DRY_RUN=1; shift ;;
  151. -y|--yes) YES=1; shift ;;
  152. --local) LOCAL_CHECKOUT="$2"; shift 2 ;;
  153. --base) BASE="$2"; shift 2 ;;
  154. -h|--help) usage 0 ;;
  155. *) echo "Unknown arg: $1" >&2; usage 2 ;;
  156. esac
  157. done
  158. # =============================================================================
  159. # Preflight
  160. # =============================================================================
  161. die() { echo "ERROR: $*" >&2; exit 1; }
  162. command -v rsync >/dev/null || die "rsync not found in PATH"
  163. command -v git >/dev/null || die "git not found in PATH"
  164. command -v gh >/dev/null || die "gh not found — install GitHub CLI"
  165. command -v python3 >/dev/null || die "python3 not found in PATH"
  166. gh auth status >/dev/null 2>&1 || die "gh not authenticated — run 'gh auth login'"
  167. [[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout"
  168. [[ -f "$UPSTREAM/package.json" ]] || die "upstream has no package.json — cannot read version"
  169. # Read the upstream version from package.json
  170. UPSTREAM_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$UPSTREAM/package.json")"
  171. [[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from upstream package.json"
  172. UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)"
  173. UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)"
  174. UPSTREAM_SHORT="$(cd "$UPSTREAM" && git rev-parse --short HEAD)"
  175. confirm() {
  176. [[ $YES -eq 1 ]] && return 0
  177. read -rp "$1 [y/N] " ans
  178. [[ "$ans" == "y" || "$ans" == "Y" ]]
  179. }
  180. if [[ "$UPSTREAM_BRANCH" != "main" ]]; then
  181. echo "WARNING: upstream is on '$UPSTREAM_BRANCH', not 'main'"
  182. confirm "Sync from '$UPSTREAM_BRANCH' anyway?" || exit 1
  183. fi
  184. UPSTREAM_STATUS="$(cd "$UPSTREAM" && git status --porcelain)"
  185. if [[ -n "$UPSTREAM_STATUS" ]]; then
  186. echo "WARNING: upstream has uncommitted changes:"
  187. echo "$UPSTREAM_STATUS" | sed 's/^/ /'
  188. echo "Sync will use working-tree state, not HEAD ($UPSTREAM_SHORT)."
  189. confirm "Continue anyway?" || exit 1
  190. fi
  191. # =============================================================================
  192. # Prepare destination (clone fork fresh, or use --local)
  193. # =============================================================================
  194. CLEANUP_DIR=""
  195. cleanup() {
  196. [[ -n "$CLEANUP_DIR" ]] && rm -rf "$CLEANUP_DIR"
  197. }
  198. trap cleanup EXIT
  199. if [[ -n "$LOCAL_CHECKOUT" ]]; then
  200. DEST_REPO="$(cd "$LOCAL_CHECKOUT" && pwd)"
  201. [[ -d "$DEST_REPO/.git" ]] || die "--local path '$DEST_REPO' is not a git checkout"
  202. else
  203. echo "Cloning $FORK..."
  204. CLEANUP_DIR="$(mktemp -d)"
  205. DEST_REPO="$CLEANUP_DIR/openai-codex-plugins"
  206. gh repo clone "$FORK" "$DEST_REPO" >/dev/null
  207. fi
  208. DEST="$DEST_REPO/$DEST_REL"
  209. # Checkout base branch
  210. cd "$DEST_REPO"
  211. git checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
  212. [[ -d "$DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — merge the bootstrap PR first, or pass --base <branch>"
  213. # =============================================================================
  214. # Create sync branch
  215. # =============================================================================
  216. TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)"
  217. SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
  218. git checkout -q -b "$SYNC_BRANCH"
  219. # =============================================================================
  220. # Build rsync args (excludes only — no protects, overlays are regenerated)
  221. # =============================================================================
  222. RSYNC_ARGS=(-av --delete)
  223. for pat in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$pat"); done
  224. # =============================================================================
  225. # Dry run preview (always shown)
  226. # =============================================================================
  227. echo ""
  228. echo "Upstream: $UPSTREAM ($UPSTREAM_BRANCH @ $UPSTREAM_SHORT)"
  229. echo "Version: $UPSTREAM_VERSION"
  230. echo "Fork: $FORK"
  231. echo "Base: $BASE"
  232. echo "Branch: $SYNC_BRANCH"
  233. echo ""
  234. echo "=== Preview (rsync --dry-run) ==="
  235. rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$DEST/"
  236. echo "=== End preview ==="
  237. echo ""
  238. echo "Overlay files (.codex-plugin/plugin.json, agents/openai.yaml) will be"
  239. echo "regenerated with version $UPSTREAM_VERSION regardless of rsync output."
  240. if [[ $DRY_RUN -eq 1 ]]; then
  241. echo ""
  242. echo "Dry run only. Nothing was changed or pushed."
  243. exit 0
  244. fi
  245. # =============================================================================
  246. # Apply
  247. # =============================================================================
  248. echo ""
  249. confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; }
  250. echo ""
  251. echo "Syncing upstream content..."
  252. rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/"
  253. echo "Regenerating overlay files..."
  254. generate_plugin_json "$DEST/.codex-plugin/plugin.json" "$UPSTREAM_VERSION"
  255. generate_agents_openai_yaml "$DEST/agents/openai.yaml"
  256. # Bail early if nothing actually changed
  257. cd "$DEST_REPO"
  258. if [[ -z "$(git status --porcelain "$DEST_REL")" ]]; then
  259. echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)."
  260. exit 0
  261. fi
  262. # =============================================================================
  263. # Commit, push, open PR
  264. # =============================================================================
  265. git add "$DEST_REL"
  266. git commit --quiet -m "sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT
  267. Automated sync via scripts/sync-to-codex-plugin.sh
  268. Upstream: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
  269. Branch: $SYNC_BRANCH"
  270. echo "Pushing $SYNC_BRANCH to $FORK..."
  271. git push -u origin "$SYNC_BRANCH" --quiet
  272. PR_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
  273. PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
  274. Run via: \`scripts/sync-to-codex-plugin.sh\`
  275. Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
  276. 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."
  277. echo "Opening PR..."
  278. PR_URL="$(gh pr create \
  279. --repo "$FORK" \
  280. --base "$BASE" \
  281. --head "$SYNC_BRANCH" \
  282. --title "$PR_TITLE" \
  283. --body "$PR_BODY")"
  284. PR_NUM="${PR_URL##*/}"
  285. DIFF_URL="https://github.com/$FORK/pull/$PR_NUM/files"
  286. echo ""
  287. echo "PR opened: $PR_URL"
  288. echo "Diff view: $DIFF_URL"