mirror of
https://github.com/obra/superpowers.git
synced 2026-04-24 18:49:07 +08:00
Compare commits
24 Commits
v5.0.7
...
wip/commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b7d3633b0 | ||
|
|
b55764852a | ||
|
|
9f42444ab1 | ||
|
|
99e4c656bf | ||
|
|
a5dd364e42 | ||
|
|
c4bbe651cb | ||
|
|
34c17aefb2 | ||
|
|
f9b088f7b3 | ||
|
|
bc25777c6a | ||
|
|
bcdd7fa24c | ||
|
|
6149f3635a | ||
|
|
777a9770d8 | ||
|
|
da283df058 | ||
|
|
a569527b89 | ||
|
|
ac1c715ffb | ||
|
|
8c8c5e87ce | ||
|
|
a5d36b1300 | ||
|
|
917e5f53b1 | ||
|
|
a6b1a1fa0c | ||
|
|
b7a8f76985 | ||
|
|
4b1b20f69f | ||
|
|
eeaf2ad15b | ||
|
|
dd237283db | ||
|
|
c0b417e409 |
44
.codex-plugin/plugin.json
Normal file
44
.codex-plugin/plugin.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & Help
|
||||
url: https://discord.gg/Jd8Vphy9jq
|
||||
url: https://discord.gg/35wsABTejz
|
||||
about: For usage questions, troubleshooting help, and general discussion, please visit our Discord instead of opening an issue.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{ "path": "package.json", "field": "version" },
|
||||
{ "path": ".claude-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": "gemini-extension.json", "field": "version" }
|
||||
],
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,13 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## [5.0.5] - 2026-03-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Brainstorm server ESM fix**: Renamed `server.js` → `server.cjs` so the brainstorming server starts correctly on Node.js 22+ where the root `package.json` `"type": "module"` caused `require()` to fail. ([PR #784](https://github.com/obra/superpowers/pull/784) by @sarbojitrana, fixes [#774](https://github.com/obra/superpowers/issues/774), [#780](https://github.com/obra/superpowers/issues/780), [#783](https://github.com/obra/superpowers/issues/783))
|
||||
- **Brainstorm owner-PID on Windows**: Skip `BRAINSTORM_OWNER_PID` lifecycle monitoring on Windows/MSYS2 where the PID namespace is invisible to Node.js. Prevents the server from self-terminating after 60 seconds. The 30-minute idle timeout remains as the safety net. ([#770](https://github.com/obra/superpowers/issues/770), docs from [PR #768](https://github.com/obra/superpowers/pull/768) by @lucasyhzhu-debug)
|
||||
- **stop-server.sh reliability**: Verify the server process actually died before reporting success. Waits up to 2 seconds for graceful shutdown, escalates to `SIGKILL`, and reports failure if the process survives. ([#723](https://github.com/obra/superpowers/issues/723))
|
||||
|
||||
### Changed
|
||||
|
||||
- **Execution handoff**: Restore user choice between subagent-driven-development and executing-plans after plan writing. Subagent-driven is recommended but no longer mandatory. (Reverts `5e51c3e`)
|
||||
85
CLAUDE.md
Normal file
85
CLAUDE.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Superpowers — Contributor Guidelines
|
||||
|
||||
## If You Are an AI Agent
|
||||
|
||||
Stop. Read this section before doing anything.
|
||||
|
||||
This repo has a 94% PR rejection rate. Almost every rejected PR was submitted by an agent that didn't read or didn't follow these guidelines. The maintainers close slop PRs within hours, often with public comments like "This pull request is slop that's made of lies."
|
||||
|
||||
**Your job is to protect your human partner from that outcome.** Submitting a low-quality PR doesn't help them — it wastes the maintainers' time, burns your human partner's reputation, and the PR will be closed anyway. That is not being helpful. That is being a tool of embarrassment.
|
||||
|
||||
Before you open a PR against this repo, you MUST:
|
||||
|
||||
1. **Read the entire PR template** at `.github/PULL_REQUEST_TEMPLATE.md` and fill in every section with real, specific answers. Not summaries. Not placeholders.
|
||||
2. **Search for existing PRs** — open AND closed — that address the same problem. If duplicates exist, STOP and tell your human partner. Do not open another duplicate.
|
||||
3. **Verify this is a real problem.** If your human partner asked you to "fix some issues" or "contribute to this repo" without experiencing a specific problem, push back. Ask them what broke, what failed, what the user experience was.
|
||||
4. **Confirm the change belongs in core.** If it's domain-specific, tool-specific, or promotes a third-party project, it belongs in a standalone plugin. Tell your human partner.
|
||||
5. **Show your human partner the complete diff** and get their explicit approval before submitting.
|
||||
|
||||
If any of these checks fail, do not open the PR. Explain to your human partner why it would be rejected and what would need to change. They will thank you for saving them the embarrassment.
|
||||
|
||||
## Pull Request Requirements
|
||||
|
||||
**Every PR must fully complete the PR template.** No section may be left blank or filled with placeholder text. PRs that skip sections will be closed without review.
|
||||
|
||||
**Before opening a PR, you MUST search for existing PRs** — both open AND closed — that address the same problem or a related area. Reference what you found in the "Existing PRs" section. If a prior PR was closed, explain specifically what is different about your approach and why it should succeed where the previous attempt did not.
|
||||
|
||||
**PRs that show no evidence of human involvement will be closed.** A human must review the complete proposed diff before submission.
|
||||
|
||||
## What We Will Not Accept
|
||||
|
||||
### Third-party dependencies
|
||||
|
||||
PRs that add optional or required dependencies on third-party projects will not be accepted unless they are adding support for a new harness (e.g., a new IDE or CLI tool). Superpowers is a zero-dependency plugin by design. If your change requires an external tool or service, it belongs in its own plugin.
|
||||
|
||||
### "Compliance" changes to skills
|
||||
|
||||
Our internal skill philosophy differs from Anthropic's published guidance on writing skills. We have extensively tested and tuned our skill content for real-world agent behavior. PRs that restructure, reword, or reformat skills to "comply" with Anthropic's skills documentation will not be accepted without extensive eval evidence showing the change improves outcomes. The bar for modifying behavior-shaping content is very high.
|
||||
|
||||
### Project-specific or personal configuration
|
||||
|
||||
Skills, hooks, or configuration that only benefit a specific project, team, domain, or workflow do not belong in core. Publish these as a separate plugin.
|
||||
|
||||
### Bulk or spray-and-pray PRs
|
||||
|
||||
Do not trawl the issue tracker and open PRs for multiple issues in a single session. Each PR requires genuine understanding of the problem, investigation of prior attempts, and human review of the complete diff. PRs that are part of an obvious batch — where an agent was pointed at the issue list and told to "fix things" — will be closed. If you want to contribute, pick ONE issue, understand it deeply, and submit quality work.
|
||||
|
||||
### Speculative or theoretical fixes
|
||||
|
||||
Every PR must solve a real problem that someone actually experienced. "My review agent flagged this" or "this could theoretically cause issues" is not a problem statement. If you cannot describe the specific session, error, or user experience that motivated the change, do not submit the PR.
|
||||
|
||||
### Domain-specific skills
|
||||
|
||||
Superpowers core contains general-purpose skills that benefit all users regardless of their project. Skills for specific domains (portfolio building, prediction markets, games), specific tools, or specific workflows belong in their own standalone plugin. Ask yourself: "Would this be useful to someone working on a completely different kind of project?" If not, publish it separately.
|
||||
|
||||
### Fork-specific changes
|
||||
|
||||
If you maintain a fork with customizations, do not open PRs to sync your fork or push fork-specific changes upstream. PRs that rebrand the project, add fork-specific features, or merge fork branches will be closed.
|
||||
|
||||
### Fabricated content
|
||||
|
||||
PRs containing invented claims, fabricated problem descriptions, or hallucinated functionality will be closed immediately. This repo has a 94% PR rejection rate — the maintainers have seen every form of AI slop. They will notice.
|
||||
|
||||
### Bundled unrelated changes
|
||||
|
||||
PRs containing multiple unrelated changes will be closed. Split them into separate PRs.
|
||||
|
||||
## Skill Changes Require Evaluation
|
||||
|
||||
Skills are not prose — they are code that shapes agent behavior. If you modify skill content:
|
||||
|
||||
- Use `superpowers:writing-skills` to develop and test changes
|
||||
- Run adversarial pressure testing across multiple sessions
|
||||
- Show before/after eval results in your PR
|
||||
- Do not modify carefully-tuned content (Red Flags tables, rationalization lists, "human partner" language) without evidence the change is an improvement
|
||||
|
||||
## Understand the Project Before Contributing
|
||||
|
||||
Before proposing changes to skill design, workflow philosophy, or architecture, read existing skills and understand the project's design decisions. Superpowers has its own tested philosophy about skill design, agent behavior shaping, and terminology (e.g., "your human partner" is deliberate, not interchangeable with "the user"). Changes that rewrite the project's voice or restructure its approach without understanding why it exists will be rejected.
|
||||
|
||||
## General
|
||||
|
||||
- Read `.github/PULL_REQUEST_TEMPLATE.md` before submitting
|
||||
- One problem per PR
|
||||
- Test on at least one harness and report results in the environment table
|
||||
- Describe the problem you solved, not just what you changed
|
||||
72
README.md
72
README.md
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
@@ -26,19 +26,21 @@ Thanks!
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
/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:
|
||||
|
||||
@@ -52,6 +54,29 @@ Then install the plugin from this 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)
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
Tell OpenCode:
|
||||
@@ -101,10 +116,6 @@ To update:
|
||||
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
|
||||
|
||||
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
|
||||
- **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
|
||||
|
||||
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
|
||||
2. Create a branch for your skill
|
||||
3. Follow the `writing-skills` skill for creating and testing new skills
|
||||
4. Submit a PR
|
||||
2. Switch to the 'dev' branch
|
||||
3. Create a branch for your work
|
||||
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.
|
||||
|
||||
## Updating
|
||||
|
||||
Skills update automatically when you update the plugin:
|
||||
|
||||
```bash
|
||||
/plugin update superpowers
|
||||
```
|
||||
Superpowers updates are somewhat coding-agent dependent, but are often automatic.
|
||||
|
||||
## License
|
||||
|
||||
@@ -185,10 +193,6 @@ MIT License - see LICENSE file for details
|
||||
|
||||
Superpowers is built by [Jesse Vincent](https://blog.fsck.com) and the rest of the folks at [Prime Radiant](https://primeradiant.com).
|
||||
|
||||
For community support, questions, and sharing what you're building with Superpowers, join us on [Discord](https://discord.gg/Jd8Vphy9jq).
|
||||
|
||||
## Support
|
||||
|
||||
- **Discord**: [Join us on Discord](https://discord.gg/Jd8Vphy9jq)
|
||||
- **Discord**: [Join us](https://discord.gg/35wsABTejz) for community support, questions, and sharing what you're building with Superpowers
|
||||
- **Issues**: https://github.com/obra/superpowers/issues
|
||||
- **Marketplace**: https://github.com/obra/superpowers-marketplace
|
||||
- **Release announcements**: [Sign up](https://primeradiant.com/superpowers/) to get notified about new versions
|
||||
|
||||
BIN
assets/app-icon.png
Normal file
BIN
assets/app-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
1
assets/superpowers-small.svg
Normal file
1
assets/superpowers-small.svg
Normal 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 |
430
scripts/sync-to-codex-plugin.sh
Executable file
430
scripts/sync-to-codex-plugin.sh
Executable file
@@ -0,0 +1,430 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# sync-to-codex-plugin.sh
|
||||
#
|
||||
# Sync this superpowers checkout → prime-radiant-inc/openai-codex-plugins.
|
||||
# Clones the fork fresh into a temp dir, rsyncs tracked upstream plugin content
|
||||
# (including committed Codex files under .codex-plugin/ and assets/), commits,
|
||||
# pushes a sync branch, and opens a PR.
|
||||
# Path/user agnostic — auto-detects upstream from script location.
|
||||
#
|
||||
# Deterministic: running twice against the same upstream SHA produces PRs with
|
||||
# identical diffs, so two back-to-back runs can verify the tool itself.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/sync-to-codex-plugin.sh # full run
|
||||
# ./scripts/sync-to-codex-plugin.sh -n # dry run
|
||||
# ./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 --base BRANCH # default: main
|
||||
# ./scripts/sync-to-codex-plugin.sh --bootstrap # create plugin dir if missing
|
||||
#
|
||||
# Bootstrap mode: skips the "plugin must exist on base" requirement and creates
|
||||
# plugins/superpowers/ when absent, then copies the tracked plugin files from
|
||||
# upstream just like a normal sync.
|
||||
#
|
||||
# Requires: bash, rsync, git, gh (authenticated), python3.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# Config — edit as upstream or canonical plugin shape evolves
|
||||
# =============================================================================
|
||||
|
||||
FORK="prime-radiant-inc/openai-codex-plugins"
|
||||
DEFAULT_BASE="main"
|
||||
DEST_REL="plugins/superpowers"
|
||||
|
||||
# Paths in upstream that should NOT land in the embedded plugin.
|
||||
# All patterns use a leading "/" to anchor them to the source root.
|
||||
# Unanchored patterns like "scripts/" would match any directory named
|
||||
# "scripts" at any depth — including legitimate nested dirs like
|
||||
# skills/brainstorming/scripts/. Anchoring prevents that.
|
||||
# (.DS_Store is intentionally unanchored — Finder creates them everywhere.)
|
||||
EXCLUDES=(
|
||||
# Dotfiles and infra — top-level only
|
||||
"/.claude/"
|
||||
"/.claude-plugin/"
|
||||
"/.codex/"
|
||||
"/.cursor-plugin/"
|
||||
"/.git/"
|
||||
"/.gitattributes"
|
||||
"/.github/"
|
||||
"/.gitignore"
|
||||
"/.opencode/"
|
||||
"/.version-bump.json"
|
||||
"/.worktrees/"
|
||||
".DS_Store"
|
||||
|
||||
# Root ceremony files
|
||||
"/AGENTS.md"
|
||||
"/CHANGELOG.md"
|
||||
"/CLAUDE.md"
|
||||
"/GEMINI.md"
|
||||
"/RELEASE-NOTES.md"
|
||||
"/gemini-extension.json"
|
||||
"/package.json"
|
||||
|
||||
# Directories not shipped by canonical Codex plugins
|
||||
"/commands/"
|
||||
"/docs/"
|
||||
"/hooks/"
|
||||
"/lib/"
|
||||
"/scripts/"
|
||||
"/tests/"
|
||||
"/tmp/"
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Ignored-path helpers
|
||||
# =============================================================================
|
||||
|
||||
IGNORED_DIR_EXCLUDES=()
|
||||
|
||||
path_has_directory_exclude() {
|
||||
local path="$1"
|
||||
local dir
|
||||
|
||||
if [[ ${#IGNORED_DIR_EXCLUDES[@]} -eq 0 ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
for dir in "${IGNORED_DIR_EXCLUDES[@]}"; do
|
||||
[[ "$path" == "$dir"* ]] && return 0
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Args
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
UPSTREAM="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BASE="$DEFAULT_BASE"
|
||||
DRY_RUN=0
|
||||
YES=0
|
||||
LOCAL_CHECKOUT=""
|
||||
BOOTSTRAP=0
|
||||
|
||||
usage() {
|
||||
sed -n '/^# Usage:/,/^# Requires:/s/^# \{0,1\}//p' "$0"
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n|--dry-run) DRY_RUN=1; shift ;;
|
||||
-y|--yes) YES=1; shift ;;
|
||||
--local) LOCAL_CHECKOUT="$2"; shift 2 ;;
|
||||
--base) BASE="$2"; shift 2 ;;
|
||||
--bootstrap) BOOTSTRAP=1; shift ;;
|
||||
-h|--help) usage 0 ;;
|
||||
*) echo "Unknown arg: $1" >&2; usage 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# =============================================================================
|
||||
# Preflight
|
||||
# =============================================================================
|
||||
|
||||
die() { echo "ERROR: $*" >&2; exit 1; }
|
||||
|
||||
command -v rsync >/dev/null || die "rsync not found in PATH"
|
||||
command -v git >/dev/null || die "git not found in PATH"
|
||||
command -v gh >/dev/null || die "gh not found — install GitHub CLI"
|
||||
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'"
|
||||
|
||||
[[ -d "$UPSTREAM/.git" ]] || die "upstream '$UPSTREAM' is not a git checkout"
|
||||
[[ -f "$UPSTREAM/.codex-plugin/plugin.json" ]] || die "committed Codex manifest missing at $UPSTREAM/.codex-plugin/plugin.json"
|
||||
|
||||
# Read the upstream version from the committed Codex manifest.
|
||||
UPSTREAM_VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$UPSTREAM/.codex-plugin/plugin.json")"
|
||||
[[ -n "$UPSTREAM_VERSION" ]] || die "could not read 'version' from committed Codex manifest"
|
||||
|
||||
UPSTREAM_BRANCH="$(cd "$UPSTREAM" && git branch --show-current)"
|
||||
UPSTREAM_SHA="$(cd "$UPSTREAM" && git rev-parse HEAD)"
|
||||
UPSTREAM_SHORT="$(cd "$UPSTREAM" && git rev-parse --short HEAD)"
|
||||
|
||||
confirm() {
|
||||
[[ $YES -eq 1 ]] && return 0
|
||||
read -rp "$1 [y/N] " ans
|
||||
[[ "$ans" == "y" || "$ans" == "Y" ]]
|
||||
}
|
||||
|
||||
if [[ "$UPSTREAM_BRANCH" != "main" ]]; then
|
||||
echo "WARNING: upstream is on '$UPSTREAM_BRANCH', not 'main'"
|
||||
confirm "Sync from '$UPSTREAM_BRANCH' anyway?" || exit 1
|
||||
fi
|
||||
|
||||
UPSTREAM_STATUS="$(cd "$UPSTREAM" && git status --porcelain)"
|
||||
if [[ -n "$UPSTREAM_STATUS" ]]; then
|
||||
echo "WARNING: upstream has uncommitted changes:"
|
||||
echo "$UPSTREAM_STATUS" | sed 's/^/ /'
|
||||
echo "Sync will use working-tree state, not HEAD ($UPSTREAM_SHORT)."
|
||||
confirm "Continue anyway?" || exit 1
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Prepare destination (clone fork fresh, or use --local)
|
||||
# =============================================================================
|
||||
|
||||
CLEANUP_DIR=""
|
||||
cleanup() {
|
||||
if [[ -n "$CLEANUP_DIR" ]]; then
|
||||
rm -rf "$CLEANUP_DIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -n "$LOCAL_CHECKOUT" ]]; then
|
||||
DEST_REPO="$(cd "$LOCAL_CHECKOUT" && pwd)"
|
||||
[[ -d "$DEST_REPO/.git" ]] || die "--local path '$DEST_REPO' is not a git checkout"
|
||||
else
|
||||
echo "Cloning $FORK..."
|
||||
CLEANUP_DIR="$(mktemp -d)"
|
||||
DEST_REPO="$CLEANUP_DIR/openai-codex-plugins"
|
||||
gh repo clone "$FORK" "$DEST_REPO" >/dev/null
|
||||
fi
|
||||
|
||||
DEST="$DEST_REPO/$DEST_REL"
|
||||
PREVIEW_REPO="$DEST_REPO"
|
||||
PREVIEW_DEST="$DEST"
|
||||
|
||||
overlay_destination_paths() {
|
||||
local repo="$1"
|
||||
local path
|
||||
local source_path
|
||||
local preview_path
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
source_path="$repo/$path"
|
||||
preview_path="$PREVIEW_REPO/$path"
|
||||
|
||||
if [[ -e "$source_path" ]]; then
|
||||
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)"
|
||||
if [[ $BOOTSTRAP -eq 1 ]]; then
|
||||
SYNC_BRANCH="bootstrap/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
|
||||
else
|
||||
SYNC_BRANCH="sync/superpowers-${UPSTREAM_SHORT}-${TIMESTAMP}"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Build rsync args
|
||||
# =============================================================================
|
||||
|
||||
RSYNC_ARGS=(-av --delete --delete-excluded)
|
||||
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)
|
||||
# =============================================================================
|
||||
|
||||
echo ""
|
||||
echo "Upstream: $UPSTREAM ($UPSTREAM_BRANCH @ $UPSTREAM_SHORT)"
|
||||
echo "Version: $UPSTREAM_VERSION"
|
||||
echo "Fork: $FORK"
|
||||
echo "Base: $BASE"
|
||||
echo "Branch: $SYNC_BRANCH"
|
||||
if [[ $BOOTSTRAP -eq 1 ]]; then
|
||||
echo "Mode: BOOTSTRAP (creating plugins/superpowers/ when absent)"
|
||||
fi
|
||||
echo ""
|
||||
echo "=== Preview (rsync --dry-run) ==="
|
||||
rsync "${RSYNC_ARGS[@]}" --dry-run --itemize-changes "$UPSTREAM/" "$PREVIEW_DEST/"
|
||||
echo "=== End preview ==="
|
||||
echo ""
|
||||
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo ""
|
||||
echo "Dry run only. Nothing was changed or pushed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Apply
|
||||
# =============================================================================
|
||||
|
||||
echo ""
|
||||
confirm "Apply changes, push branch, and open PR?" || { echo "Aborted."; exit 1; }
|
||||
|
||||
echo ""
|
||||
if [[ -n "$LOCAL_CHECKOUT" ]]; then
|
||||
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
|
||||
|
||||
apply_to_preview_checkout
|
||||
if ! preview_checkout_has_changes; then
|
||||
echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
prepare_apply_checkout
|
||||
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
|
||||
cd "$DEST_REPO"
|
||||
if [[ -z "$(git status --porcelain "$DEST_REL")" ]]; then
|
||||
echo "No changes — embedded plugin was already in sync with upstream $UPSTREAM_SHORT (v$UPSTREAM_VERSION)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Commit, push, open PR
|
||||
# =============================================================================
|
||||
|
||||
git add "$DEST_REL"
|
||||
|
||||
if [[ $BOOTSTRAP -eq 1 ]]; then
|
||||
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).
|
||||
|
||||
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\`
|
||||
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
|
||||
|
||||
This is a one-time bootstrap. Subsequent syncs will be normal (non-bootstrap) runs using the same tracked upstream plugin files."
|
||||
else
|
||||
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).
|
||||
|
||||
Copies the tracked plugin files from upstream, including the committed Codex manifest and assets.
|
||||
|
||||
Run via: \`scripts/sync-to-codex-plugin.sh\`
|
||||
Upstream commit: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
|
||||
|
||||
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."
|
||||
fi
|
||||
|
||||
git commit --quiet -m "$COMMIT_TITLE
|
||||
|
||||
Automated sync via scripts/sync-to-codex-plugin.sh
|
||||
Upstream: https://github.com/obra/superpowers/commit/$UPSTREAM_SHA
|
||||
Branch: $SYNC_BRANCH"
|
||||
|
||||
echo "Pushing $SYNC_BRANCH to $FORK..."
|
||||
git push -u origin "$SYNC_BRANCH" --quiet
|
||||
|
||||
echo "Opening PR..."
|
||||
PR_URL="$(gh pr create \
|
||||
--repo "$FORK" \
|
||||
--base "$BASE" \
|
||||
--head "$SYNC_BRANCH" \
|
||||
--title "$COMMIT_TITLE" \
|
||||
--body "$PR_BODY")"
|
||||
|
||||
PR_NUM="${PR_URL##*/}"
|
||||
DIFF_URL="https://github.com/$FORK/pull/$PR_NUM/files"
|
||||
|
||||
echo ""
|
||||
echo "PR opened: $PR_URL"
|
||||
echo "Diff view: $DIFF_URL"
|
||||
571
tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
Executable file
571
tests/codex-plugin-sync/test-sync-to-codex-plugin.sh
Executable 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 "$@"
|
||||
Reference in New Issue
Block a user