Compare commits

..

5 Commits

Author SHA1 Message Date
Jesse Vincent
0b7d3633b0 Use committed Codex plugin files in sync script
- commit .codex-plugin/plugin.json and brand assets in this repo
- sync tracked Codex plugin files instead of generating or seeding them
- honor upstream gitignored files during rsync
- cover the new sync behavior with regression tests
2026-04-23 18:31:11 -07:00
Jesse Vincent
b55764852a formatting 2026-04-16 12:50:46 -07:00
Jesse Vincent
9f42444ab1 formatting 2026-04-16 12:50:46 -07:00
Jesse Vincent
99e4c656bf reorder installs 2026-04-16 12:50:46 -07:00
Jesse Vincent
a5dd364e42 README updates for Codex, other cleanup 2026-04-16 12:50:46 -07:00
7 changed files with 815 additions and 148 deletions

44
.codex-plugin/plugin.json Normal file
View File

@@ -0,0 +1,44 @@
{
"name": "superpowers",
"version": "5.0.7",
"description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.",
"author": {
"name": "Jesse Vincent",
"email": "jesse@fsck.com",
"url": "https://github.com/obra"
},
"homepage": "https://github.com/obra/superpowers",
"repository": "https://github.com/obra/superpowers",
"license": "MIT",
"keywords": [
"brainstorming",
"subagent-driven-development",
"skills",
"planning",
"tdd",
"debugging",
"code-review",
"workflow"
],
"skills": "./skills/",
"interface": {
"displayName": "Superpowers",
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
"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.",
"developerName": "Jesse Vincent",
"category": "Coding",
"capabilities": [
"Interactive",
"Read",
"Write"
],
"defaultPrompt": [
"I've got an idea for something I'd like to build.",
"Let's add a feature to this project."
],
"brandColor": "#F59E0B",
"composerIcon": "./assets/superpowers-small.svg",
"logo": "./assets/app-icon.png",
"screenshots": []
}
}

View File

@@ -3,6 +3,7 @@
{ "path": "package.json", "field": "version" }, { "path": "package.json", "field": "version" },
{ "path": ".claude-plugin/plugin.json", "field": "version" }, { "path": ".claude-plugin/plugin.json", "field": "version" },
{ "path": ".cursor-plugin/plugin.json", "field": "version" }, { "path": ".cursor-plugin/plugin.json", "field": "version" },
{ "path": ".codex-plugin/plugin.json", "field": "version" },
{ "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" }, { "path": ".claude-plugin/marketplace.json", "field": "plugins.0.version" },
{ "path": "gemini-extension.json", "field": "version" } { "path": "gemini-extension.json", "field": "version" }
], ],

View File

@@ -1,6 +1,6 @@
# Superpowers # Superpowers
Superpowers is a complete software development workflow for your coding agents, built on top of a set of composable "skills" and some initial instructions that make sure your agent uses them. Superpowers is a complete software development methodology for your coding agents, built on top of a set of composable skills and some initial instructions that make sure your agent uses them.
## How it works ## How it works
@@ -26,19 +26,21 @@ Thanks!
## Installation ## Installation
**Note:** Installation differs by platform. Claude Code or Cursor have built-in plugin marketplaces. Codex and OpenCode require manual setup. **Note:** Installation differs by platform.
### Claude Code Official Marketplace ### Claude Code Official Marketplace
Superpowers is available via the [official Claude plugin marketplace](https://claude.com/plugins/superpowers) Superpowers is available via the [official Claude plugin marketplace](https://claude.com/plugins/superpowers)
Install the plugin from Claude marketplace: Install the plugin from Anthropic's official marketplace:
```bash ```bash
/plugin install superpowers@claude-plugins-official /plugin install superpowers@claude-plugins-official
``` ```
### Claude Code (via Plugin Marketplace) ### Claude Code (Superpowers Marketplace)
The Superpowers marketplace provides Superpowers and some other related plugins for Claude Code.
In Claude Code, register the marketplace first: In Claude Code, register the marketplace first:
@@ -52,6 +54,29 @@ Then install the plugin from this marketplace:
/plugin install superpowers@superpowers-marketplace /plugin install superpowers@superpowers-marketplace
``` ```
### OpenAI Codex CLI
- Open plugin search interface
```bash
/plugins
```
Search for Superpowers
```bash
superpowers
```
Select `Install Plugin`
### OpenAI Codex App
- In the Codex app, click on Plugins in the sidebar.
- You should see `Superpowers` in the Coding section.
- Click the `+` next to Superpowers and follow the prompts.
### Cursor (via Plugin Marketplace) ### Cursor (via Plugin Marketplace)
In Cursor Agent chat, install from marketplace: In Cursor Agent chat, install from marketplace:
@@ -62,16 +87,6 @@ In Cursor Agent chat, install from marketplace:
or search for "superpowers" in the plugin marketplace. or search for "superpowers" in the plugin marketplace.
### Codex
Tell Codex:
```
Fetch and follow instructions from https://raw.githubusercontent.com/obra/superpowers/refs/heads/main/.codex/INSTALL.md
```
**Detailed docs:** [docs/README.codex.md](docs/README.codex.md)
### OpenCode ### OpenCode
Tell OpenCode: Tell OpenCode:
@@ -101,10 +116,6 @@ To update:
gemini extensions update superpowers gemini extensions update superpowers
``` ```
### Verify Installation
Start a new session in your chosen platform and ask for something that should trigger a skill (for example, "help me plan this feature" or "let's debug this issue"). The agent should automatically invoke the relevant superpowers skill.
## The Basic Workflow ## The Basic Workflow
1. **brainstorming** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation. Saves design document. 1. **brainstorming** - Activates before writing code. Refines rough ideas through questions, explores alternatives, presents design in sections for validation. Saves design document.
@@ -156,26 +167,23 @@ Start a new session in your chosen platform and ask for something that should tr
- **Complexity reduction** - Simplicity as primary goal - **Complexity reduction** - Simplicity as primary goal
- **Evidence over claims** - Verify before declaring success - **Evidence over claims** - Verify before declaring success
Read more: [Superpowers for Claude Code](https://blog.fsck.com/2025/10/09/superpowers/) Read [the original release announcement](https://blog.fsck.com/2025/10/09/superpowers/).
## Contributing ## Contributing
Skills live directly in this repository. To contribute: The general contribution process for Superpowers is below. Keep in mind that we don't generally accept contributions of new skills and that any updates to skills must work across all of the coding agents we support.
1. Fork the repository 1. Fork the repository
2. Create a branch for your skill 2. Switch to the 'dev' branch
3. Follow the `writing-skills` skill for creating and testing new skills 3. Create a branch for your work
4. Submit a PR 4. Follow the `writing-skills` skill for creating and testing new and modified skills
5. Submit a PR, being sure to fill in the pull request template.
See `skills/writing-skills/SKILL.md` for the complete guide. See `skills/writing-skills/SKILL.md` for the complete guide.
## Updating ## Updating
Skills update automatically when you update the plugin: Superpowers updates are somewhat coding-agent dependent, but are often automatic.
```bash
/plugin update superpowers
```
## License ## License

BIN
assets/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Calque_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M394.28,207.8c.81,2.41,1.39,4.78,1.8,7.07,1.61,9.03-.93,17.78-5.99,21.74-22.6,17.7-49.85,29.35-75.34,38.6-.59.22-1.09.28-1.4.34-2.22.47-4.95,1.04-7.25,0-1.46-.66-2.25-1.74-2.66-2.3-1.56-2.1-1.59-4.31-1.56-5.13.1-2.67-.01-4.69,0-4.82.45-3.52.91-10.66,1.41-21.28.6-3.87,2.16-9.63,6.94-13.96,4.01-3.62,8.33-4.6,14.59-5.87,10.76-2.19,37.21-8.22,47.42-16.56,1.63-1.33,2.97-2.65,4.19-3.96,3.72-3.99,6.39-7.92,7.93-10.36,3.22,3.22,7.25,8.48,9.92,16.47Z"/><path d="M428.67,185.28c-2.33,11.99-8.91,22.32-15.88,30.38.27-5.5-.05-12.11-1.86-19.08-5.04-19.36-19.74-34.7-37.78-37.78-32.21-9.74-70.59,3.79-99.08,18.29-3.87,1.95-9.52-2.77-11.84-8.16-3.32-7.71-1.63-6.28,2.61-8.49,38.31-20.03,82.01-39.61,123.91-29.7,8.26,1.95,15.96,5.26,23.48,10.54,11.32,7.96,20.21,24.74,16.44,44Z"/><path d="M117.72,304.2c-.81-2.41-1.39-4.78-1.8-7.07-1.61-9.03.93-17.78,5.99-21.74,22.6-17.7,49.85-29.35,75.34-38.6.59-.22,1.09-.28,1.4-.34,2.22-.47,4.95-1.04,7.25,0,1.46.66,2.25,1.74,2.66,2.3,1.56,2.1,1.59,4.31,1.56,5.13-.1,2.67.01,4.69,0,4.82-.45,3.52-.91,10.66-1.41,21.28-.6,3.87-2.16,9.63-6.94,13.96-4.01,3.62-8.33,4.6-14.59,5.87-10.76,2.19-37.21,8.22-47.42,16.56-1.63,1.33-2.97,2.65-4.19,3.96-3.72,3.99-6.39,7.92-7.93,10.36-3.22-3.22-7.25-8.48-9.92-16.47Z"/><path d="M83.33,326.72c2.33-11.99,8.91-22.32,15.88-30.38-.27,5.5.05,12.11,1.86,19.08,5.04,19.36,19.74,34.7,37.78,37.78,32.21,9.74,70.59-3.79,99.08-18.29,3.87-1.95,9.52,2.77,11.84,8.16,3.32,7.71,1.63,6.28-2.61,8.49-38.31,20.03-82.01,39.61-123.91,29.7-8.26-1.95-15.96-5.26-23.48-10.54-11.32-7.96-20.21-24.74-16.44-44Z"/><ellipse cx="255.16" cy="258.86" rx="28.95" ry="28.76"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -3,9 +3,9 @@
# sync-to-codex-plugin.sh # sync-to-codex-plugin.sh
# #
# Sync this superpowers checkout → prime-radiant-inc/openai-codex-plugins. # Sync this superpowers checkout → prime-radiant-inc/openai-codex-plugins.
# Clones the fork fresh into a temp dir, rsyncs upstream content, regenerates # Clones the fork fresh into a temp dir, rsyncs tracked upstream plugin content
# the Codex overlay file (.codex-plugin/plugin.json) inline, commits, pushes a # (including committed Codex files under .codex-plugin/ and assets/), commits,
# sync branch, and opens a PR. # pushes a sync branch, and opens a PR.
# Path/user agnostic — auto-detects upstream from script location. # Path/user agnostic — auto-detects upstream from script location.
# #
# Deterministic: running twice against the same upstream SHA produces PRs with # Deterministic: running twice against the same upstream SHA produces PRs with
@@ -17,13 +17,11 @@
# ./scripts/sync-to-codex-plugin.sh -y # skip confirm # ./scripts/sync-to-codex-plugin.sh -y # skip confirm
# ./scripts/sync-to-codex-plugin.sh --local PATH # existing checkout # ./scripts/sync-to-codex-plugin.sh --local PATH # existing checkout
# ./scripts/sync-to-codex-plugin.sh --base BRANCH # default: main # ./scripts/sync-to-codex-plugin.sh --base BRANCH # default: main
# ./scripts/sync-to-codex-plugin.sh --bootstrap --assets-src DIR # create initial plugin # ./scripts/sync-to-codex-plugin.sh --bootstrap # create plugin dir if missing
# #
# Bootstrap mode: skips the "plugin must exist on base" check and seeds # Bootstrap mode: skips the "plugin must exist on base" requirement and creates
# plugins/superpowers/assets/ from --assets-src <dir> which must contain # plugins/superpowers/ when absent, then copies the tracked plugin files from
# PrimeRadiant_Favicon.svg and PrimeRadiant_Favicon.png. Run once by one # upstream just like a normal sync.
# team member to create the initial PR; every subsequent run is a normal
# (non-bootstrap) sync.
# #
# Requires: bash, rsync, git, gh (authenticated), python3. # Requires: bash, rsync, git, gh (authenticated), python3.
@@ -38,9 +36,6 @@ DEFAULT_BASE="main"
DEST_REL="plugins/superpowers" DEST_REL="plugins/superpowers"
# Paths in upstream that should NOT land in the embedded plugin. # Paths in upstream that should NOT land in the embedded plugin.
# The Codex-only paths are here too — they're managed by generate/bootstrap
# steps, not by rsync.
#
# All patterns use a leading "/" to anchor them to the source root. # All patterns use a leading "/" to anchor them to the source root.
# Unanchored patterns like "scripts/" would match any directory named # Unanchored patterns like "scripts/" would match any directory named
# "scripts" at any depth — including legitimate nested dirs like # "scripts" at any depth — including legitimate nested dirs like
@@ -78,68 +73,57 @@ EXCLUDES=(
"/scripts/" "/scripts/"
"/tests/" "/tests/"
"/tmp/" "/tmp/"
# Codex-only paths — managed outside rsync
"/.codex-plugin/"
"/assets/"
) )
# ============================================================================= # =============================================================================
# Generated overlay file # Ignored-path helpers
# ============================================================================= # =============================================================================
# Writes the Codex plugin manifest to "$1" with the given upstream version. IGNORED_DIR_EXCLUDES=()
# Args: dest_path, version
generate_plugin_json() { path_has_directory_exclude() {
local dest="$1" local path="$1"
local version="$2" local dir
mkdir -p "$(dirname "$dest")"
cat > "$dest" <<EOF if [[ ${#IGNORED_DIR_EXCLUDES[@]} -eq 0 ]]; then
{ return 1
"name": "superpowers", fi
"version": "$version",
"description": "An agentic skills framework & software development methodology that works: planning, TDD, debugging, and collaboration workflows.", for dir in "${IGNORED_DIR_EXCLUDES[@]}"; do
"author": { [[ "$path" == "$dir"* ]] && return 0
"name": "Jesse Vincent", done
"email": "jesse@fsck.com",
"url": "https://github.com/obra" return 1
},
"homepage": "https://github.com/obra/superpowers",
"repository": "https://github.com/obra/superpowers",
"license": "MIT",
"keywords": [
"brainstorming",
"subagent-driven-development",
"skills",
"planning",
"tdd",
"debugging",
"code-review",
"workflow"
],
"skills": "./skills/",
"interface": {
"displayName": "Superpowers",
"shortDescription": "Planning, TDD, debugging, and delivery workflows for coding agents",
"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.",
"developerName": "Jesse Vincent",
"category": "Coding",
"capabilities": [
"Interactive",
"Read",
"Write"
],
"defaultPrompt": [
"I've got an idea for something I'd like to build.",
"Let's add a feature to this project."
],
"brandColor": "#F59E0B",
"composerIcon": "./assets/superpowers-small.svg",
"logo": "./assets/app-icon.png",
"screenshots": []
}
} }
EOF
ignored_directory_has_tracked_descendants() {
local path="$1"
[[ -n "$(git -C "$UPSTREAM" ls-files --cached -- "$path/")" ]]
}
append_git_ignored_directory_excludes() {
local path
local lookup_path
while IFS= read -r -d '' path; do
[[ "$path" == */ ]] || continue
lookup_path="${path%/}"
if ! ignored_directory_has_tracked_descendants "$lookup_path"; then
IGNORED_DIR_EXCLUDES+=("$path")
RSYNC_ARGS+=(--exclude="/$path")
fi
done < <(git -C "$UPSTREAM" ls-files --others --ignored --exclude-standard --directory -z)
}
append_git_ignored_file_excludes() {
local path
while IFS= read -r -d '' path; do
path_has_directory_exclude "$path" && continue
RSYNC_ARGS+=(--exclude="/$path")
done < <(git -C "$UPSTREAM" ls-files --others --ignored --exclude-standard -z)
} }
# ============================================================================= # =============================================================================
@@ -153,10 +137,9 @@ DRY_RUN=0
YES=0 YES=0
LOCAL_CHECKOUT="" LOCAL_CHECKOUT=""
BOOTSTRAP=0 BOOTSTRAP=0
ASSETS_SRC=""
usage() { usage() {
sed -n 's/^# \{0,1\}//;2,27p' "$0" sed -n '/^# Usage:/,/^# Requires:/s/^# \{0,1\}//p' "$0"
exit "${1:-0}" exit "${1:-0}"
} }
@@ -167,7 +150,6 @@ while [[ $# -gt 0 ]]; do
--local) LOCAL_CHECKOUT="$2"; shift 2 ;; --local) LOCAL_CHECKOUT="$2"; shift 2 ;;
--base) BASE="$2"; shift 2 ;; --base) BASE="$2"; shift 2 ;;
--bootstrap) BOOTSTRAP=1; shift ;; --bootstrap) BOOTSTRAP=1; shift ;;
--assets-src) ASSETS_SRC="$2"; shift 2 ;;
-h|--help) usage 0 ;; -h|--help) usage 0 ;;
*) echo "Unknown arg: $1" >&2; usage 2 ;; *) echo "Unknown arg: $1" >&2; usage 2 ;;
esac esac
@@ -187,19 +169,11 @@ command -v python3 >/dev/null || die "python3 not found in PATH"
gh auth status >/dev/null 2>&1 || die "gh not authenticated — run 'gh auth login'" gh auth status >/dev/null 2>&1 || die "gh not authenticated — run 'gh auth login'"
[[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout" [[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout"
[[ -f "$UPSTREAM/package.json" ]] || die "upstream has no package.json — cannot read version" [[ -f "$UPSTREAM/.codex-plugin/plugin.json" ]] || die "committed Codex manifest missing at $UPSTREAM/.codex-plugin/plugin.json"
# Bootstrap-mode validation # Read the upstream version from the committed Codex manifest.
if [[ $BOOTSTRAP -eq 1 ]]; then UPSTREAM_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$UPSTREAM/.codex-plugin/plugin.json")"
[[ -n "$ASSETS_SRC" ]] || die "--bootstrap requires --assets-src <path>" [[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from committed Codex manifest"
ASSETS_SRC="$(cd "$ASSETS_SRC" 2>/dev/null && pwd)" || die "assets source '$ASSETS_SRC' is not a directory"
[[ -f "$ASSETS_SRC/PrimeRadiant_Favicon.svg" ]] || die "assets source missing PrimeRadiant_Favicon.svg"
[[ -f "$ASSETS_SRC/PrimeRadiant_Favicon.png" ]] || die "assets source missing PrimeRadiant_Favicon.png"
fi
# Read the upstream version from package.json
UPSTREAM_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$UPSTREAM/package.json")"
[[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from upstream package.json"
UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)" UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)"
UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)" UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)"
@@ -230,7 +204,9 @@ fi
CLEANUP_DIR="" CLEANUP_DIR=""
cleanup() { cleanup() {
[[ -n "$CLEANUP_DIR" ]] && rm -rf "$CLEANUP_DIR" if [[ -n "$CLEANUP_DIR" ]]; then
rm -rf "$CLEANUP_DIR"
fi
} }
trap cleanup EXIT trap cleanup EXIT
@@ -245,22 +221,84 @@ else
fi fi
DEST="$DEST_REPO/$DEST_REL" DEST="$DEST_REPO/$DEST_REL"
PREVIEW_REPO="$DEST_REPO"
PREVIEW_DEST="$DEST"
# Checkout base branch overlay_destination_paths() {
cd "$DEST_REPO" local repo="$1"
git checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK" local path
local source_path
local preview_path
# Plugin-existence check depends on mode while IFS= read -r -d '' path; do
if [[ $BOOTSTRAP -eq 1 ]]; then source_path="$repo/$path"
[[ ! -d "$DEST" ]] || die "--bootstrap but base branch '$BASE' already has '$DEST_REL/' — use normal sync instead" preview_path="$PREVIEW_REPO/$path"
mkdir -p "$DEST"
else
[[ -d "$DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap + --assets-src, or pass --base <branch>"
fi
# ============================================================================= if [[ -e "$source_path" ]]; then
# Create sync branch mkdir -p "$(dirname "$preview_path")"
# ============================================================================= cp -R "$source_path" "$preview_path"
else
rm -rf "$preview_path"
fi
done
}
copy_local_destination_overlay() {
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" diff --name-only -z -- "$DEST_REL"
)
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" diff --cached --name-only -z -- "$DEST_REL"
)
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" ls-files --others --exclude-standard -z -- "$DEST_REL"
)
overlay_destination_paths "$DEST_REPO" < <(
git -C "$DEST_REPO" ls-files --others --ignored --exclude-standard -z -- "$DEST_REL"
)
}
local_checkout_has_uncommitted_destination_changes() {
[[ -n "$(git -C "$DEST_REPO" status --porcelain=1 --untracked-files=all --ignored=matching -- "$DEST_REL")" ]]
}
prepare_preview_checkout() {
if [[ -n "$LOCAL_CHECKOUT" ]]; then
[[ -n "$CLEANUP_DIR" ]] || CLEANUP_DIR="$(mktemp -d)"
PREVIEW_REPO="$CLEANUP_DIR/preview"
git clone -q --no-local "$DEST_REPO" "$PREVIEW_REPO"
PREVIEW_DEST="$PREVIEW_REPO/$DEST_REL"
fi
git -C "$PREVIEW_REPO" checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
if [[ -n "$LOCAL_CHECKOUT" ]]; then
copy_local_destination_overlay
fi
if [[ $BOOTSTRAP -ne 1 ]]; then
[[ -d "$PREVIEW_DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap, or pass --base <branch>"
fi
}
prepare_apply_checkout() {
git -C "$DEST_REPO" checkout -q "$BASE" 2>/dev/null || die "base branch '$BASE' doesn't exist in $FORK"
if [[ $BOOTSTRAP -ne 1 ]]; then
[[ -d "$DEST" ]] || die "base branch '$BASE' has no '$DEST_REL/' — use --bootstrap, or pass --base <branch>"
fi
}
apply_to_preview_checkout() {
if [[ $BOOTSTRAP -eq 1 ]]; then
mkdir -p "$PREVIEW_DEST"
fi
rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$PREVIEW_DEST/"
}
preview_checkout_has_changes() {
[[ -n "$(git -C "$PREVIEW_REPO" status --porcelain "$DEST_REL")" ]]
}
prepare_preview_checkout
TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)" TIMESTAMP="$(date -u +%Y%m%d-%H%M%S)"
if [[ $BOOTSTRAP -eq 1 ]]; then if [[ $BOOTSTRAP -eq 1 ]]; then
@@ -268,14 +306,15 @@ if [[ $BOOTSTRAP -eq 1 ]]; then
else else
SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}" SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
fi fi
git checkout -q -b "$SYNC_BRANCH"
# ============================================================================= # =============================================================================
# Build rsync args # Build rsync args
# ============================================================================= # =============================================================================
RSYNC_ARGS=(-av --delete) RSYNC_ARGS=(-av --delete --delete-excluded)
for pat in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$pat"); done for pat in "${EXCLUDES[@]}"; do RSYNC_ARGS+=(--exclude="$pat"); done
append_git_ignored_directory_excludes
append_git_ignored_file_excludes
# ============================================================================= # =============================================================================
# Dry run preview (always shown) # Dry run preview (always shown)
@@ -288,20 +327,13 @@ echo "Fork: $FORK"
echo "Base: $BASE" echo "Base: $BASE"
echo "Branch: $SYNC_BRANCH" echo "Branch: $SYNC_BRANCH"
if [[ $BOOTSTRAP -eq 1 ]]; then if [[ $BOOTSTRAP -eq 1 ]]; then
echo "Mode: BOOTSTRAP (creating initial plugin from scratch)" echo "Mode: BOOTSTRAP (creating plugins/superpowers/ when absent)"
echo "Assets: $ASSETS_SRC"
fi fi
echo "" echo ""
echo "=== Preview (rsync --dry-run) ===" echo "=== Preview (rsync --dry-run) ==="
rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$DEST/" rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$PREVIEW_DEST/"
echo "=== End preview ===" echo "=== End preview ==="
echo "" echo ""
echo "Overlay file (.codex-plugin/plugin.json) will be regenerated with"
echo "version $UPSTREAM_VERSION regardless of rsync output."
if [[ $BOOTSTRAP -eq 1 ]]; then
echo "Assets (superpowers-small.svg, app-icon.png) will be seeded from:"
echo " $ASSETS_SRC"
fi
if [[ $DRY_RUN -eq 1 ]]; then if [[ $DRY_RUN -eq 1 ]]; then
echo "" echo ""
@@ -317,18 +349,26 @@ echo ""
confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; } confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; }
echo "" echo ""
echo "Syncing upstream content..." if [[ -n "$LOCAL_CHECKOUT" ]]; then
rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/" if local_checkout_has_uncommitted_destination_changes; then
die "local checkout has uncommitted changes under '$DEST_REL' — commit, stash, or discard them before syncing"
fi
if [[ $BOOTSTRAP -eq 1 ]]; then apply_to_preview_checkout
echo "Seeding brand assets..." if ! preview_checkout_has_changes; then
mkdir -p "$DEST/assets" echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)."
cp "$ASSETS_SRC/PrimeRadiant_Favicon.svg" "$DEST/assets/superpowers-small.svg" exit 0
cp "$ASSETS_SRC/PrimeRadiant_Favicon.png" "$DEST/assets/app-icon.png" fi
fi fi
echo "Regenerating overlay file..." prepare_apply_checkout
generate_plugin_json "$DEST/.codex-plugin/plugin.json" "$UPSTREAM_VERSION" cd "$DEST_REPO"
git checkout -q -b "$SYNC_BRANCH"
echo "Syncing upstream content..."
if [[ $BOOTSTRAP -eq 1 ]]; then
mkdir -p "$DEST"
fi
rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$DEST/"
# Bail early if nothing actually changed # Bail early if nothing actually changed
cd "$DEST_REPO" cd "$DEST_REPO"
@@ -347,16 +387,18 @@ if [[ $BOOTSTRAP -eq 1 ]]; then
COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT" COMMIT_TITLE="bootstrap superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION). PR_BODY="Initial bootstrap of the superpowers plugin from upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
Creates \`plugins/superpowers/\` from scratch: upstream content via rsync, \`.codex-plugin/plugin.json\` regenerated inline, brand assets seeded from a local Brand Assets directory. Creates \`plugins/superpowers/\` by copying the tracked plugin files from upstream, including \`.codex-plugin/plugin.json\` and \`assets/\`.
Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap --assets-src <path>\` Run via: \`scripts/sync-to-codex-plugin.sh --bootstrap\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs and will not touch the \`assets/\` directory." This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files."
else else
COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT" COMMIT_TITLE="sync superpowers v$UPSTREAM_VERSION from upstream main @ $UPSTREAM_SHORT"
PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION). PR_BODY="Automated sync from superpowers upstream \`main\` @ \`$UPSTREAM_SHORT\` (v$UPSTREAM_VERSION).
Copies the tracked plugin files from upstream, including the committed Codex manifest and assets.
Run via: \`scripts/sync-to-codex-plugin.sh\` Run via: \`scripts/sync-to-codex-plugin.sh\`
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA

View File

@@ -0,0 +1,571 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
SYNC_SCRIPT_SOURCE="$REPO_ROOT/scripts/sync-to-codex-plugin.sh"
BASH_UNDER_TEST="/bin/bash"
PACKAGE_VERSION="1.2.3"
MANIFEST_VERSION="9.8.7"
FAILURES=0
TEST_ROOT=""
pass() {
echo " [PASS] $1"
}
fail() {
echo " [FAIL] $1"
FAILURES=$((FAILURES + 1))
}
assert_equals() {
local actual="$1"
local expected="$2"
local description="$3"
if [[ "$actual" == "$expected" ]]; then
pass "$description"
else
fail "$description"
echo " expected: $expected"
echo " actual: $actual"
fi
}
assert_contains() {
local haystack="$1"
local needle="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
pass "$description"
else
fail "$description"
echo " expected to find: $needle"
fi
}
assert_not_contains() {
local haystack="$1"
local needle="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Fq -- "$needle"; then
fail "$description"
echo " did not expect to find: $needle"
else
pass "$description"
fi
}
assert_matches() {
local haystack="$1"
local pattern="$2"
local description="$3"
if printf '%s' "$haystack" | grep -Eq -- "$pattern"; then
pass "$description"
else
fail "$description"
echo " expected to match: $pattern"
fi
}
assert_path_absent() {
local path="$1"
local description="$2"
if [[ ! -e "$path" ]]; then
pass "$description"
else
fail "$description"
echo " did not expect path to exist: $path"
fi
}
assert_branch_absent() {
local repo="$1"
local pattern="$2"
local description="$3"
local branches
branches="$(git -C "$repo" branch --list "$pattern")"
if [[ -z "$branches" ]]; then
pass "$description"
else
fail "$description"
echo " did not expect matching branches:"
echo "$branches" | sed 's/^/ /'
fi
}
assert_current_branch() {
local repo="$1"
local expected="$2"
local description="$3"
local actual
actual="$(git -C "$repo" branch --show-current)"
assert_equals "$actual" "$expected" "$description"
}
assert_file_equals() {
local path="$1"
local expected="$2"
local description="$3"
local actual
actual="$(cat "$path")"
assert_equals "$actual" "$expected" "$description"
}
cleanup() {
if [[ -n "$TEST_ROOT" && -d "$TEST_ROOT" ]]; then
rm -rf "$TEST_ROOT"
fi
}
configure_git_identity() {
local repo="$1"
git -C "$repo" config user.name "Test Bot"
git -C "$repo" config user.email "test@example.com"
}
init_repo() {
local repo="$1"
git init -q -b main "$repo"
configure_git_identity "$repo"
}
commit_fixture() {
local repo="$1"
local message="$2"
git -C "$repo" commit -q -m "$message"
}
checkout_fixture_branch() {
local repo="$1"
local branch="$2"
git -C "$repo" checkout -q -b "$branch"
}
write_upstream_fixture() {
local repo="$1"
local with_pure_ignored="${2:-1}"
mkdir -p \
"$repo/.codex-plugin" \
"$repo/.private-journal" \
"$repo/assets" \
"$repo/scripts" \
"$repo/skills/example"
if [[ "$with_pure_ignored" == "1" ]]; then
mkdir -p "$repo/ignored-cache/tmp"
fi
cp "$SYNC_SCRIPT_SOURCE" "$repo/scripts/sync-to-codex-plugin.sh"
cat > "$repo/package.json" <<EOF
{
"name": "fixture-upstream",
"version": "$PACKAGE_VERSION"
}
EOF
cat > "$repo/.gitignore" <<'EOF'
.private-journal/
EOF
if [[ "$with_pure_ignored" == "1" ]]; then
cat >> "$repo/.gitignore" <<'EOF'
ignored-cache/
EOF
fi
cat > "$repo/.codex-plugin/plugin.json" <<EOF
{
"name": "superpowers",
"version": "$MANIFEST_VERSION"
}
EOF
cat > "$repo/assets/superpowers-small.svg" <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"></svg>
EOF
printf 'png fixture\n' > "$repo/assets/app-icon.png"
cat > "$repo/skills/example/SKILL.md" <<'EOF'
# Example Skill
Fixture content.
EOF
printf 'tracked keep\n' > "$repo/.private-journal/keep.txt"
printf 'ignored leak\n' > "$repo/.private-journal/leak.txt"
if [[ "$with_pure_ignored" == "1" ]]; then
printf 'ignored cache state\n' > "$repo/ignored-cache/tmp/state.json"
fi
git -C "$repo" add \
.codex-plugin/plugin.json \
.gitignore \
assets/app-icon.png \
assets/superpowers-small.svg \
package.json \
scripts/sync-to-codex-plugin.sh \
skills/example/SKILL.md
git -C "$repo" add -f .private-journal/keep.txt
commit_fixture "$repo" "Initial upstream fixture"
}
write_destination_fixture() {
local repo="$1"
mkdir -p "$repo/plugins/superpowers/skills/example"
printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
Fixture content.
EOF
git -C "$repo" add plugins/superpowers/.fixture-keep
git -C "$repo" add plugins/superpowers/skills/example/SKILL.md
commit_fixture "$repo" "Initial destination fixture"
}
dirty_tracked_destination_skill() {
local repo="$1"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
Locally modified fixture content.
EOF
}
write_synced_destination_fixture() {
local repo="$1"
mkdir -p \
"$repo/plugins/superpowers/.codex-plugin" \
"$repo/plugins/superpowers/.private-journal" \
"$repo/plugins/superpowers/assets" \
"$repo/plugins/superpowers/skills/example"
cat > "$repo/plugins/superpowers/.codex-plugin/plugin.json" <<EOF
{
"name": "superpowers",
"version": "$MANIFEST_VERSION"
}
EOF
cat > "$repo/plugins/superpowers/assets/superpowers-small.svg" <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"></svg>
EOF
printf 'png fixture\n' > "$repo/plugins/superpowers/assets/app-icon.png"
cat > "$repo/plugins/superpowers/skills/example/SKILL.md" <<'EOF'
# Example Skill
Fixture content.
EOF
printf 'tracked keep\n' > "$repo/plugins/superpowers/.private-journal/keep.txt"
git -C "$repo" add \
plugins/superpowers/.codex-plugin/plugin.json \
plugins/superpowers/assets/app-icon.png \
plugins/superpowers/assets/superpowers-small.svg \
plugins/superpowers/skills/example/SKILL.md \
plugins/superpowers/.private-journal/keep.txt
commit_fixture "$repo" "Initial synced destination fixture"
}
write_stale_ignored_destination_fixture() {
local repo="$1"
mkdir -p "$repo/plugins/superpowers/.private-journal"
printf 'fixture keep\n' > "$repo/plugins/superpowers/.fixture-keep"
printf 'stale ignored leak\n' > "$repo/plugins/superpowers/.private-journal/leak.txt"
git -C "$repo" add plugins/superpowers/.fixture-keep
commit_fixture "$repo" "Initial stale ignored destination fixture"
}
write_fake_gh() {
local bin_dir="$1"
mkdir -p "$bin_dir"
cat > "$bin_dir/gh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
if [[ "${1:-}" == "auth" && "${2:-}" == "status" ]]; then
exit 0
fi
echo "unexpected gh invocation: $*" >&2
exit 1
EOF
chmod +x "$bin_dir/gh"
}
run_preview() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --local "$dest" 2>&1
}
run_bootstrap_preview() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --bootstrap --local "$dest" 2>&1
}
run_preview_without_manifest() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
rm -f "$upstream/.codex-plugin/plugin.json"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --local "$dest" 2>&1
}
run_preview_with_stale_ignored_destination() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -n --local "$dest" 2>&1
}
run_apply() {
local upstream="$1"
local dest="$2"
local fake_bin="$3"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" -y --local "$dest" 2>&1
}
run_help() {
local upstream="$1"
local fake_bin="$2"
PATH="$fake_bin:$PATH" "$BASH_UNDER_TEST" "$upstream/scripts/sync-to-codex-plugin.sh" --help 2>&1
}
write_bootstrap_destination_fixture() {
local repo="$1"
printf 'bootstrap fixture\n' > "$repo/README.md"
git -C "$repo" add README.md
commit_fixture "$repo" "Initial bootstrap destination fixture"
}
main() {
local upstream
local mixed_only_upstream
local dest
local dest_branch
local mixed_only_dest
local stale_dest
local dirty_apply_dest
local dirty_apply_dest_branch
local noop_apply_dest
local noop_apply_dest_branch
local fake_bin
local bootstrap_dest
local bootstrap_dest_branch
local preview_status
local preview_output
local preview_section
local bootstrap_status
local bootstrap_output
local missing_manifest_status
local missing_manifest_output
local mixed_only_status
local mixed_only_output
local stale_preview_status
local stale_preview_output
local stale_preview_section
local dirty_apply_status
local dirty_apply_output
local noop_apply_status
local noop_apply_output
local help_output
local script_source
local dirty_skill_path
echo "=== Test: sync-to-codex-plugin dry-run regression ==="
TEST_ROOT="$(mktemp -d)"
trap cleanup EXIT
upstream="$TEST_ROOT/upstream"
mixed_only_upstream="$TEST_ROOT/mixed-only-upstream"
dest="$TEST_ROOT/destination"
mixed_only_dest="$TEST_ROOT/mixed-only-destination"
stale_dest="$TEST_ROOT/stale-destination"
dirty_apply_dest="$TEST_ROOT/dirty-apply-destination"
dirty_apply_dest_branch="fixture/dirty-apply-target"
noop_apply_dest="$TEST_ROOT/noop-apply-destination"
noop_apply_dest_branch="fixture/noop-apply-target"
bootstrap_dest="$TEST_ROOT/bootstrap-destination"
dest_branch="fixture/preview-target"
bootstrap_dest_branch="fixture/bootstrap-preview-target"
fake_bin="$TEST_ROOT/bin"
init_repo "$upstream"
write_upstream_fixture "$upstream"
init_repo "$mixed_only_upstream"
write_upstream_fixture "$mixed_only_upstream" 0
init_repo "$dest"
write_destination_fixture "$dest"
checkout_fixture_branch "$dest" "$dest_branch"
dirty_tracked_destination_skill "$dest"
init_repo "$mixed_only_dest"
write_destination_fixture "$mixed_only_dest"
init_repo "$stale_dest"
write_stale_ignored_destination_fixture "$stale_dest"
init_repo "$dirty_apply_dest"
write_synced_destination_fixture "$dirty_apply_dest"
checkout_fixture_branch "$dirty_apply_dest" "$dirty_apply_dest_branch"
dirty_tracked_destination_skill "$dirty_apply_dest"
init_repo "$noop_apply_dest"
write_synced_destination_fixture "$noop_apply_dest"
checkout_fixture_branch "$noop_apply_dest" "$noop_apply_dest_branch"
init_repo "$bootstrap_dest"
write_bootstrap_destination_fixture "$bootstrap_dest"
checkout_fixture_branch "$bootstrap_dest" "$bootstrap_dest_branch"
write_fake_gh "$fake_bin"
# This regression test is about dry-run content, so capture the preview
# output even if the current script exits nonzero in --local mode.
set +e
preview_output="$(run_preview "$upstream" "$dest" "$fake_bin")"
preview_status=$?
bootstrap_output="$(run_bootstrap_preview "$upstream" "$bootstrap_dest" "$fake_bin")"
bootstrap_status=$?
mixed_only_output="$(run_preview "$mixed_only_upstream" "$mixed_only_dest" "$fake_bin")"
mixed_only_status=$?
stale_preview_output="$(run_preview_with_stale_ignored_destination "$upstream" "$stale_dest" "$fake_bin")"
stale_preview_status=$?
dirty_apply_output="$(run_apply "$upstream" "$dirty_apply_dest" "$fake_bin")"
dirty_apply_status=$?
noop_apply_output="$(run_apply "$upstream" "$noop_apply_dest" "$fake_bin")"
noop_apply_status=$?
missing_manifest_output="$(run_preview_without_manifest "$upstream" "$dest" "$fake_bin")"
missing_manifest_status=$?
set -e
help_output="$(run_help "$upstream" "$fake_bin")"
script_source="$(cat "$upstream/scripts/sync-to-codex-plugin.sh")"
preview_section="$(printf '%s\n' "$preview_output" | sed -n '/^=== Preview (rsync --dry-run) ===$/,/^=== End preview ===$/p')"
stale_preview_section="$(printf '%s\n' "$stale_preview_output" | sed -n '/^=== Preview (rsync --dry-run) ===$/,/^=== End preview ===$/p')"
dirty_skill_path="$dirty_apply_dest/plugins/superpowers/skills/example/SKILL.md"
echo ""
echo "Preview assertions..."
assert_equals "$preview_status" "0" "Preview exits successfully"
assert_contains "$preview_output" "Version: $MANIFEST_VERSION" "Preview uses manifest version"
assert_not_contains "$preview_output" "Version: $PACKAGE_VERSION" "Preview does not use package.json version"
assert_contains "$preview_section" ".codex-plugin/plugin.json" "Preview includes manifest path"
assert_contains "$preview_section" "assets/superpowers-small.svg" "Preview includes SVG asset"
assert_contains "$preview_section" "assets/app-icon.png" "Preview includes PNG asset"
assert_contains "$preview_section" ".private-journal/keep.txt" "Preview includes tracked ignored file"
assert_not_contains "$preview_section" ".private-journal/leak.txt" "Preview excludes ignored untracked file"
assert_not_contains "$preview_section" "ignored-cache/" "Preview excludes pure ignored directories"
assert_not_contains "$preview_output" "Overlay file (.codex-plugin/plugin.json) will be regenerated" "Preview omits overlay regeneration note"
assert_not_contains "$preview_output" "Assets (superpowers-small.svg, app-icon.png) will be seeded from" "Preview omits assets seeding note"
assert_contains "$preview_section" "skills/example/SKILL.md" "Preview reflects dirty tracked destination file"
assert_current_branch "$dest" "$dest_branch" "Preview leaves destination checkout on its original branch"
assert_branch_absent "$dest" "sync/superpowers-*" "Preview does not create sync branch in destination checkout"
echo ""
echo "Mixed-directory assertions..."
assert_equals "$mixed_only_status" "0" "Mixed ignored directory preview exits successfully under /bin/bash"
assert_contains "$mixed_only_output" ".private-journal/keep.txt" "Mixed ignored directory preview still includes tracked ignored file"
assert_not_contains "$mixed_only_output" "ignored-cache/" "Mixed ignored directory preview has no pure ignored directory fixture"
echo ""
echo "Convergence assertions..."
assert_equals "$stale_preview_status" "0" "Stale ignored destination preview exits successfully"
assert_matches "$stale_preview_section" "\\*deleting +\\.private-journal/leak\\.txt" "Preview deletes stale ignored destination file"
echo ""
echo "Bootstrap assertions..."
assert_equals "$bootstrap_status" "0" "Bootstrap preview exits successfully"
assert_contains "$bootstrap_output" "Mode: BOOTSTRAP (creating plugins/superpowers/ when absent)" "Bootstrap preview describes directory creation"
assert_not_contains "$bootstrap_output" "Assets:" "Bootstrap preview omits external assets path"
assert_contains "$bootstrap_output" "Dry run only. Nothing was changed or pushed." "Bootstrap preview remains dry-run only"
assert_path_absent "$bootstrap_dest/plugins/superpowers" "Bootstrap preview does not create destination plugin directory"
assert_current_branch "$bootstrap_dest" "$bootstrap_dest_branch" "Bootstrap preview leaves destination checkout on its original branch"
assert_branch_absent "$bootstrap_dest" "bootstrap/superpowers-*" "Bootstrap preview does not create bootstrap branch in destination checkout"
echo ""
echo "Apply assertions..."
assert_equals "$dirty_apply_status" "1" "Dirty local apply exits with failure"
assert_contains "$dirty_apply_output" "ERROR: local checkout has uncommitted changes under 'plugins/superpowers'" "Dirty local apply reports protected destination path"
assert_current_branch "$dirty_apply_dest" "$dirty_apply_dest_branch" "Dirty local apply leaves destination checkout on its original branch"
assert_branch_absent "$dirty_apply_dest" "sync/superpowers-*" "Dirty local apply does not create sync branch in destination checkout"
assert_file_equals "$dirty_skill_path" "# Example Skill
Locally modified fixture content." "Dirty local apply preserves tracked working-tree file content"
assert_equals "$noop_apply_status" "0" "Clean no-op local apply exits successfully"
assert_contains "$noop_apply_output" "No changes — embedded plugin was already in sync with upstream" "Clean no-op local apply reports no changes"
assert_current_branch "$noop_apply_dest" "$noop_apply_dest_branch" "Clean no-op local apply leaves destination checkout on its original branch"
assert_branch_absent "$noop_apply_dest" "sync/superpowers-*" "Clean no-op local apply does not create sync branch in destination checkout"
echo ""
echo "Missing manifest assertions..."
assert_equals "$missing_manifest_status" "1" "Missing manifest exits with failure"
assert_contains "$missing_manifest_output" "ERROR: committed Codex manifest missing at" "Missing manifest reports committed manifest path"
echo ""
echo "Help assertions..."
assert_not_contains "$help_output" "--assets-src" "Help omits --assets-src"
echo ""
echo "Source assertions..."
assert_not_contains "$script_source" "regenerated inline" "Source drops regenerated inline phrasing"
assert_not_contains "$script_source" "Brand Assets directory" "Source drops Brand Assets directory phrasing"
assert_not_contains "$script_source" "--assets-src" "Source drops --assets-src"
if [[ $FAILURES -ne 0 ]]; then
echo ""
echo "FAILED: $FAILURES assertion(s) failed."
exit 1
fi
echo ""
echo "PASS"
}
main "$@"