chore: import upstream snapshot with attribution
This commit is contained in:
Executable
+220
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# bump-version.sh — bump version numbers across all declared files,
|
||||
# with drift detection and repo-wide audit for missed files.
|
||||
#
|
||||
# Usage:
|
||||
# bump-version.sh <new-version> Bump all declared files to new version
|
||||
# bump-version.sh --check Report current versions (detect drift)
|
||||
# bump-version.sh --audit Check + grep repo for old version strings
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
CONFIG="$REPO_ROOT/.version-bump.json"
|
||||
|
||||
if [[ ! -f "$CONFIG" ]]; then
|
||||
echo "error: .version-bump.json not found at $CONFIG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- helpers ---
|
||||
|
||||
# Read a dotted field path from a JSON file.
|
||||
# Handles both simple ("version") and nested ("plugins.0.version") paths.
|
||||
read_json_field() {
|
||||
local file="$1" field="$2"
|
||||
# Convert dot-path to jq path: "plugins.0.version" -> .plugins[0].version
|
||||
local jq_path
|
||||
jq_path=$(echo "$field" | sed -E 's/\.([0-9]+)/[\1]/g' | sed 's/^/./' | sed 's/\.\././g')
|
||||
jq -r "$jq_path" "$file"
|
||||
}
|
||||
|
||||
# Write a dotted field path in a JSON file, preserving formatting.
|
||||
write_json_field() {
|
||||
local file="$1" field="$2" value="$3"
|
||||
local jq_path
|
||||
jq_path=$(echo "$field" | sed -E 's/\.([0-9]+)/[\1]/g' | sed 's/^/./' | sed 's/\.\././g')
|
||||
local tmp="${file}.tmp"
|
||||
jq "$jq_path = \"$value\"" "$file" > "$tmp" && mv "$tmp" "$file"
|
||||
}
|
||||
|
||||
# Read the list of declared files from config.
|
||||
# Outputs lines of "path<TAB>field"
|
||||
declared_files() {
|
||||
jq -r '.files[] | "\(.path)\t\(.field)"' "$CONFIG"
|
||||
}
|
||||
|
||||
# Read the audit exclude patterns from config.
|
||||
audit_excludes() {
|
||||
jq -r '.audit.exclude[]' "$CONFIG" 2>/dev/null
|
||||
}
|
||||
|
||||
# --- commands ---
|
||||
|
||||
cmd_check() {
|
||||
local has_drift=0
|
||||
local versions=()
|
||||
|
||||
echo "Version check:"
|
||||
echo ""
|
||||
|
||||
while IFS=$'\t' read -r path field; do
|
||||
local fullpath="$REPO_ROOT/$path"
|
||||
if [[ ! -f "$fullpath" ]]; then
|
||||
printf " %-45s MISSING\n" "$path ($field)"
|
||||
has_drift=1
|
||||
continue
|
||||
fi
|
||||
local ver
|
||||
ver=$(read_json_field "$fullpath" "$field")
|
||||
printf " %-45s %s\n" "$path ($field)" "$ver"
|
||||
versions+=("$ver")
|
||||
done < <(declared_files)
|
||||
|
||||
echo ""
|
||||
|
||||
# Check if all versions match
|
||||
local unique
|
||||
unique=$(printf '%s\n' "${versions[@]}" | sort -u | wc -l | tr -d ' ')
|
||||
if [[ "$unique" -gt 1 ]]; then
|
||||
echo "DRIFT DETECTED — versions are not in sync:"
|
||||
printf '%s\n' "${versions[@]}" | sort | uniq -c | sort -rn | while read -r count ver; do
|
||||
echo " $ver ($count files)"
|
||||
done
|
||||
has_drift=1
|
||||
else
|
||||
echo "All declared files are in sync at ${versions[0]}"
|
||||
fi
|
||||
|
||||
return $has_drift
|
||||
}
|
||||
|
||||
cmd_audit() {
|
||||
# First run check
|
||||
cmd_check || true
|
||||
echo ""
|
||||
|
||||
# Determine the current version (most common across declared files)
|
||||
local current_version
|
||||
current_version=$(
|
||||
while IFS=$'\t' read -r path field; do
|
||||
local fullpath="$REPO_ROOT/$path"
|
||||
[[ -f "$fullpath" ]] && read_json_field "$fullpath" "$field"
|
||||
done < <(declared_files) | sort | uniq -c | sort -rn | head -1 | awk '{print $2}'
|
||||
)
|
||||
|
||||
if [[ -z "$current_version" ]]; then
|
||||
echo "error: could not determine current version" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Audit: scanning repo for version string '$current_version'..."
|
||||
echo ""
|
||||
|
||||
# Build grep exclude args
|
||||
local -a exclude_args=()
|
||||
while IFS= read -r pattern; do
|
||||
exclude_args+=("--exclude=$pattern" "--exclude-dir=$pattern")
|
||||
done < <(audit_excludes)
|
||||
|
||||
# Also always exclude binary files and .git
|
||||
exclude_args+=("--exclude-dir=.git" "--exclude-dir=node_modules" "--binary-files=without-match")
|
||||
|
||||
# Get list of declared paths for comparison
|
||||
local -a declared_paths=()
|
||||
while IFS=$'\t' read -r path _field; do
|
||||
declared_paths+=("$path")
|
||||
done < <(declared_files)
|
||||
|
||||
# Grep for the version string
|
||||
local found_undeclared=0
|
||||
while IFS= read -r match; do
|
||||
local match_file
|
||||
match_file=$(echo "$match" | cut -d: -f1)
|
||||
# Make path relative to repo root
|
||||
local rel_path="${match_file#$REPO_ROOT/}"
|
||||
|
||||
# Check if this file is in the declared list
|
||||
local is_declared=0
|
||||
for dp in "${declared_paths[@]}"; do
|
||||
if [[ "$rel_path" == "$dp" ]]; then
|
||||
is_declared=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$is_declared" -eq 0 ]]; then
|
||||
if [[ "$found_undeclared" -eq 0 ]]; then
|
||||
echo "UNDECLARED files containing '$current_version':"
|
||||
found_undeclared=1
|
||||
fi
|
||||
echo " $match"
|
||||
fi
|
||||
done < <(grep -rn "${exclude_args[@]}" -F "$current_version" "$REPO_ROOT" 2>/dev/null || true)
|
||||
|
||||
if [[ "$found_undeclared" -eq 0 ]]; then
|
||||
echo "No undeclared files contain the version string. All clear."
|
||||
else
|
||||
echo ""
|
||||
echo "Review the above files — if they should be bumped, add them to .version-bump.json"
|
||||
echo "If they should be skipped, add them to the audit.exclude list."
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_bump() {
|
||||
local new_version="$1"
|
||||
|
||||
# Validate semver-ish format
|
||||
if ! echo "$new_version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
|
||||
echo "error: '$new_version' doesn't look like a version (expected X.Y.Z)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Bumping all declared files to $new_version..."
|
||||
echo ""
|
||||
|
||||
while IFS=$'\t' read -r path field; do
|
||||
local fullpath="$REPO_ROOT/$path"
|
||||
if [[ ! -f "$fullpath" ]]; then
|
||||
echo " SKIP (missing): $path"
|
||||
continue
|
||||
fi
|
||||
local old_ver
|
||||
old_ver=$(read_json_field "$fullpath" "$field")
|
||||
write_json_field "$fullpath" "$field" "$new_version"
|
||||
printf " %-45s %s -> %s\n" "$path ($field)" "$old_ver" "$new_version"
|
||||
done < <(declared_files)
|
||||
|
||||
echo ""
|
||||
echo "Done. Running audit to check for missed files..."
|
||||
echo ""
|
||||
cmd_audit
|
||||
}
|
||||
|
||||
# --- main ---
|
||||
|
||||
case "${1:-}" in
|
||||
--check)
|
||||
cmd_check
|
||||
;;
|
||||
--audit)
|
||||
cmd_audit
|
||||
;;
|
||||
--help|-h|"")
|
||||
echo "Usage: bump-version.sh <new-version> | --check | --audit"
|
||||
echo ""
|
||||
echo " <new-version> Bump all declared files to the given version"
|
||||
echo " --check Show current versions, detect drift"
|
||||
echo " --audit Check + scan repo for undeclared version references"
|
||||
exit 0
|
||||
;;
|
||||
--*)
|
||||
echo "error: unknown flag '$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
cmd_bump "$1"
|
||||
;;
|
||||
esac
|
||||
Executable
+211
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Lint shell scripts in this repository.
|
||||
#
|
||||
# Usage:
|
||||
# scripts/lint-shell.sh [--all] [--format] [--strict] [file ...]
|
||||
#
|
||||
# By default, runs ShellCheck and shell syntax checks on changed shell scripts.
|
||||
# Use --format to format with shfmt before linting. Use --all for the full tracked
|
||||
# baseline, or pass files explicitly to lint a smaller set.
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_tool() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "required tool '$1' is not on PATH"
|
||||
}
|
||||
|
||||
is_shell_file() {
|
||||
local path="$1"
|
||||
local first_line=""
|
||||
|
||||
[[ -f "$path" ]] || return 1
|
||||
|
||||
case "$path" in
|
||||
*.sh)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
IFS= read -r first_line <"$path" || true
|
||||
[[ "$first_line" =~ ^#!.*[/[:space:]](bash|dash|ksh|sh)([[:space:]]|$) ]]
|
||||
}
|
||||
|
||||
ensure_git_work_tree() {
|
||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
||||
|| die "run this from inside a git work tree, or pass files explicitly"
|
||||
}
|
||||
|
||||
add_shell_file() {
|
||||
local path
|
||||
local existing
|
||||
|
||||
path="$1"
|
||||
if ! is_shell_file "$path"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "${#files[@]}" -gt 0 ]]; then
|
||||
for existing in "${files[@]}"; do
|
||||
if [[ "$existing" == "$path" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
files+=("$path")
|
||||
}
|
||||
|
||||
collect_all_shell_files() {
|
||||
local path
|
||||
|
||||
ensure_git_work_tree
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
add_shell_file "$path"
|
||||
done < <(git ls-files -z)
|
||||
}
|
||||
|
||||
collect_changed_shell_files() {
|
||||
local path
|
||||
|
||||
ensure_git_work_tree
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
||||
while IFS= read -r -d '' path; do
|
||||
add_shell_file "$path"
|
||||
done < <(git diff --name-only -z --diff-filter=ACMR HEAD)
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
add_shell_file "$path"
|
||||
done < <(git diff --cached --name-only -z --diff-filter=ACMR)
|
||||
else
|
||||
collect_all_shell_files
|
||||
fi
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
add_shell_file "$path"
|
||||
done < <(git ls-files --others --exclude-standard -z)
|
||||
}
|
||||
|
||||
collect_requested_shell_files() {
|
||||
local path
|
||||
|
||||
for path in "$@"; do
|
||||
add_shell_file "$path"
|
||||
done
|
||||
}
|
||||
|
||||
syntax_shell_for() {
|
||||
local path="$1"
|
||||
local first_line=""
|
||||
|
||||
IFS= read -r first_line <"$path" || true
|
||||
|
||||
case "$first_line" in
|
||||
*"/sh"* | *" env sh"* | *"/dash"* | *" env dash"*)
|
||||
printf 'sh'
|
||||
;;
|
||||
*)
|
||||
printf 'bash'
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_syntax_checks() {
|
||||
local file
|
||||
local shell_name
|
||||
|
||||
for file in "$@"; do
|
||||
shell_name="$(syntax_shell_for "$file")"
|
||||
case "$shell_name" in
|
||||
sh)
|
||||
sh -n "$file"
|
||||
;;
|
||||
bash)
|
||||
bash -n "$file"
|
||||
;;
|
||||
*)
|
||||
die "unsupported shell for syntax check: $shell_name"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
format=false
|
||||
strict=false
|
||||
all=false
|
||||
requested_files=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--all)
|
||||
all=true
|
||||
;;
|
||||
--format)
|
||||
format=true
|
||||
;;
|
||||
--strict)
|
||||
strict=true
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
requested_files+=("$@")
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
*)
|
||||
requested_files+=("$1")
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
require_tool shellcheck
|
||||
if [[ "$format" == true ]]; then
|
||||
require_tool shfmt
|
||||
fi
|
||||
|
||||
files=()
|
||||
if [[ "${#requested_files[@]}" -gt 0 ]]; then
|
||||
collect_requested_shell_files "${requested_files[@]}"
|
||||
elif [[ "$all" == true ]]; then
|
||||
collect_all_shell_files
|
||||
else
|
||||
collect_changed_shell_files
|
||||
fi
|
||||
|
||||
if [[ "${#files[@]}" -eq 0 ]]; then
|
||||
echo "No shell files found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$format" == true ]]; then
|
||||
echo "Formatting ${#files[@]} shell files"
|
||||
shfmt_args=(-i 2 -ci -bn)
|
||||
shfmt "${shfmt_args[@]}" -w "${files[@]}"
|
||||
fi
|
||||
|
||||
echo "Linting ${#files[@]} shell files"
|
||||
|
||||
shellcheck_args=(--severity=warning --external-sources --source-path=SCRIPTDIR)
|
||||
if [[ "$strict" == true ]]; then
|
||||
shellcheck_args+=("--enable=check-extra-masked-returns,check-set-e-suppressed,quote-safe-variables,deprecate-which,avoid-nullary-conditions")
|
||||
fi
|
||||
|
||||
shellcheck "${shellcheck_args[@]}" "${files[@]}"
|
||||
run_syntax_checks "${files[@]}"
|
||||
Executable
+342
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Package the Superpowers Codex plugin as a rootless archive for portal upload.
|
||||
#
|
||||
# The Codex portal artifact differs from the old openai/plugins sync flow:
|
||||
# it is a standalone archive, but it still needs the OpenAI-owned
|
||||
# skills/*/agents/openai.yaml metadata that used to be preserved from the
|
||||
# destination plugin repo. Seed that metadata from a prior official package.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
REF="HEAD"
|
||||
OUTPUT=""
|
||||
FORMAT=""
|
||||
METADATA_SOURCE=""
|
||||
ALLOW_DIRTY=0
|
||||
KEEP_STAGE=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/package-codex-plugin.sh [options]
|
||||
|
||||
Options:
|
||||
--output PATH Write archive to PATH.
|
||||
Default: ../_tmp/sup-codex-packaging/superpowers-VERSION.zip
|
||||
--format FORMAT Archive format: zip or tar.gz. Default: zip.
|
||||
If --output ends in .zip, .tar.gz, or .tgz, that
|
||||
extension is used when --format is omitted.
|
||||
--metadata-source PATH Prior official package directory, .zip, or .tar.gz used to
|
||||
seed skills/*/agents/openai.yaml.
|
||||
Default: ../_tmp/sup-codex-packaging/superpowers,
|
||||
falling back to superpowers.zip, then superpowers.tar.gz
|
||||
--ref REF Git ref to package. Default: HEAD.
|
||||
--allow-dirty Permit a dirty working tree. The archive still uses --ref.
|
||||
--keep-stage Print and keep the temporary staging directory.
|
||||
-h, --help Show this help.
|
||||
|
||||
The archive is rootless: .codex-plugin/, assets/, skills/, README.md, LICENSE,
|
||||
and CODE_OF_CONDUCT.md sit at the archive root. Source-only repo files, hooks, tests,
|
||||
docs, and other harness manifests are intentionally not shipped.
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--output)
|
||||
[[ $# -ge 2 ]] || die "--output requires a path"
|
||||
OUTPUT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--format)
|
||||
[[ $# -ge 2 ]] || die "--format requires a value"
|
||||
case "$2" in
|
||||
zip)
|
||||
FORMAT="zip"
|
||||
;;
|
||||
tar.gz|tgz)
|
||||
FORMAT="tar.gz"
|
||||
;;
|
||||
*)
|
||||
die "--format must be zip or tar.gz"
|
||||
;;
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
--metadata-source)
|
||||
[[ $# -ge 2 ]] || die "--metadata-source requires a path"
|
||||
METADATA_SOURCE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--ref)
|
||||
[[ $# -ge 2 ]] || die "--ref requires a value"
|
||||
REF="$2"
|
||||
shift 2
|
||||
;;
|
||||
--allow-dirty)
|
||||
ALLOW_DIRTY=1
|
||||
shift
|
||||
;;
|
||||
--keep-stage)
|
||||
KEEP_STAGE=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown arg: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
infer_format_from_output() {
|
||||
local output_path="$1"
|
||||
|
||||
case "$output_path" in
|
||||
*.tar.gz|*.tgz)
|
||||
printf '%s\n' "tar.gz"
|
||||
;;
|
||||
*.zip)
|
||||
printf '%s\n' "zip"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ -z "$FORMAT" ]]; then
|
||||
FORMAT="$(infer_format_from_output "$OUTPUT" || true)"
|
||||
if [[ -z "$FORMAT" ]]; then
|
||||
FORMAT="zip"
|
||||
fi
|
||||
else
|
||||
output_format="$(infer_format_from_output "$OUTPUT" || true)"
|
||||
if [[ -n "$output_format" && "$output_format" != "$FORMAT" ]]; then
|
||||
die "--output extension does not match --format $FORMAT: $OUTPUT"
|
||||
fi
|
||||
fi
|
||||
|
||||
command -v git >/dev/null || die "git not found in PATH"
|
||||
command -v jq >/dev/null || die "jq not found in PATH"
|
||||
command -v tar >/dev/null || die "tar not found in PATH"
|
||||
command -v gzip >/dev/null || die "gzip not found in PATH"
|
||||
command -v shasum >/dev/null || die "shasum not found in PATH"
|
||||
if [[ "$FORMAT" == "zip" ]]; then
|
||||
command -v zip >/dev/null || die "zip not found in PATH"
|
||||
command -v unzip >/dev/null || die "unzip not found in PATH"
|
||||
fi
|
||||
|
||||
[[ -d "$REPO_ROOT/.git" ]] || die "repo root is not a git checkout: $REPO_ROOT"
|
||||
git -C "$REPO_ROOT" rev-parse --verify "$REF^{commit}" >/dev/null ||
|
||||
die "git ref does not resolve to a commit: $REF"
|
||||
|
||||
if [[ "$ALLOW_DIRTY" -ne 1 ]]; then
|
||||
dirty_status="$(git -C "$REPO_ROOT" status --porcelain --untracked-files=all)"
|
||||
if [[ -n "$dirty_status" ]]; then
|
||||
echo "Working tree has uncommitted changes:" >&2
|
||||
printf '%s\n' "$dirty_status" | sed 's/^/ /' >&2
|
||||
die "commit or stash changes first, or pass --allow-dirty to package $REF anyway"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$METADATA_SOURCE" ]]; then
|
||||
if [[ -d "$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers" ]]; then
|
||||
METADATA_SOURCE="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers"
|
||||
elif [[ -f "$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.zip" ]]; then
|
||||
METADATA_SOURCE="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.zip"
|
||||
elif [[ -f "$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.tar.gz" ]]; then
|
||||
METADATA_SOURCE="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers.tar.gz"
|
||||
else
|
||||
die "no metadata source found; pass --metadata-source <prior package dir, zip, or tar.gz>"
|
||||
fi
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/superpowers-codex-package.XXXXXX")"
|
||||
STAGE="$WORK_DIR/payload"
|
||||
METADATA_WORK="$WORK_DIR/metadata"
|
||||
ARCHIVE_LIST="$WORK_DIR/archive-list"
|
||||
|
||||
cleanup() {
|
||||
if [[ "$KEEP_STAGE" -eq 1 ]]; then
|
||||
echo "Keeping staging directory: $WORK_DIR" >&2
|
||||
else
|
||||
rm -rf "$WORK_DIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$STAGE" "$METADATA_WORK"
|
||||
|
||||
metadata_root_from_dir() {
|
||||
local candidate="$1"
|
||||
local nested
|
||||
|
||||
if [[ -d "$candidate/skills" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
|
||||
nested="$(find "$candidate" -mindepth 2 -maxdepth 2 -type d -name skills -print -quit)"
|
||||
if [[ -n "$nested" ]]; then
|
||||
dirname "$nested"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
prepare_metadata_root() {
|
||||
local source="$1"
|
||||
local root
|
||||
|
||||
if [[ -d "$source" ]]; then
|
||||
root="$(cd "$source" && pwd)"
|
||||
elif [[ -f "$source" ]]; then
|
||||
case "$source" in
|
||||
*.tar.gz|*.tgz)
|
||||
tar -xzf "$source" -C "$METADATA_WORK"
|
||||
root="$METADATA_WORK"
|
||||
;;
|
||||
*.zip)
|
||||
command -v unzip >/dev/null || die "unzip not found in PATH"
|
||||
unzip -q "$source" -d "$METADATA_WORK"
|
||||
root="$METADATA_WORK"
|
||||
;;
|
||||
*)
|
||||
die "metadata source must be a directory, .zip, or .tar.gz: $source"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
die "metadata source does not exist: $source"
|
||||
fi
|
||||
|
||||
metadata_root_from_dir "$root" ||
|
||||
die "metadata source does not contain a skills/ directory: $source"
|
||||
}
|
||||
|
||||
METADATA_ROOT="$(prepare_metadata_root "$METADATA_SOURCE")"
|
||||
|
||||
git -C "$REPO_ROOT" archive --format=tar "$REF" -- \
|
||||
.codex-plugin \
|
||||
CODE_OF_CONDUCT.md \
|
||||
LICENSE \
|
||||
README.md \
|
||||
assets \
|
||||
skills \
|
||||
| tar -xf - -C "$STAGE"
|
||||
|
||||
VERSION="$(jq -r '.version // empty' "$STAGE/.codex-plugin/plugin.json")"
|
||||
[[ -n "$VERSION" ]] || die "could not read version from .codex-plugin/plugin.json"
|
||||
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
case "$FORMAT" in
|
||||
zip)
|
||||
OUTPUT="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers-$VERSION.zip"
|
||||
;;
|
||||
tar.gz)
|
||||
OUTPUT="$REPO_ROOT/../_tmp/sup-codex-packaging/superpowers-$VERSION.tar.gz"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
mkdir -p "$(dirname "$OUTPUT")"
|
||||
OUTPUT="$(cd "$(dirname "$OUTPUT")" && pwd)/$(basename "$OUTPUT")"
|
||||
|
||||
missing_metadata=0
|
||||
while IFS= read -r skill_dir; do
|
||||
skill_name="${skill_dir##*/}"
|
||||
metadata_file="$METADATA_ROOT/skills/$skill_name/agents/openai.yaml"
|
||||
|
||||
if [[ ! -f "$metadata_file" ]]; then
|
||||
echo "Missing OpenAI agent metadata for skill: $skill_name" >&2
|
||||
missing_metadata=1
|
||||
continue
|
||||
fi
|
||||
|
||||
mkdir -p "$skill_dir/agents"
|
||||
cp "$metadata_file" "$skill_dir/agents/openai.yaml"
|
||||
done < <(find "$STAGE/skills" -mindepth 1 -maxdepth 1 -type d -print | sort)
|
||||
|
||||
if [[ "$missing_metadata" -ne 0 ]]; then
|
||||
die "metadata source is incomplete"
|
||||
fi
|
||||
|
||||
skill_count="$(find "$STAGE/skills" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')"
|
||||
metadata_count="$(find "$STAGE/skills" -path '*/agents/openai.yaml' -type f | wc -l | tr -d ' ')"
|
||||
[[ "$skill_count" == "$metadata_count" ]] ||
|
||||
die "metadata count mismatch: $metadata_count metadata files for $skill_count skills"
|
||||
|
||||
(
|
||||
cd "$STAGE"
|
||||
{
|
||||
find . -mindepth 1 -type d | sed 's#^\./##' | LC_ALL=C sort
|
||||
find . -mindepth 1 -type f | sed 's#^\./##' | LC_ALL=C sort
|
||||
} >"$ARCHIVE_LIST"
|
||||
)
|
||||
|
||||
case "$FORMAT" in
|
||||
zip)
|
||||
# ZIP cannot represent dates earlier than 1980.
|
||||
TZ=UTC find "$STAGE" -exec touch -t 198001010000 {} +
|
||||
(
|
||||
cd "$STAGE"
|
||||
rm -f "$OUTPUT"
|
||||
COPYFILE_DISABLE=1 zip -X -q - -@ <"$ARCHIVE_LIST" >"$OUTPUT"
|
||||
)
|
||||
;;
|
||||
tar.gz)
|
||||
# Match the prior official archive's deterministic tar entry metadata.
|
||||
TZ=UTC find "$STAGE" -exec touch -t 197001010000 {} +
|
||||
(
|
||||
cd "$STAGE"
|
||||
rm -f "$OUTPUT"
|
||||
COPYFILE_DISABLE=1 tar -cf - --no-recursion --format ustar --uid 0 --gid 0 --uname '' --gname '' -T "$ARCHIVE_LIST" |
|
||||
gzip -9n >"$OUTPUT"
|
||||
)
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v xattr >/dev/null 2>&1; then
|
||||
xattr -c "$OUTPUT" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
case "$FORMAT" in
|
||||
zip)
|
||||
archive_paths="$(unzip -Z1 "$OUTPUT" | sed 's#/$##')"
|
||||
;;
|
||||
tar.gz)
|
||||
archive_paths="$(tar -tzf "$OUTPUT")"
|
||||
;;
|
||||
esac
|
||||
|
||||
unexpected_paths="$(
|
||||
printf '%s\n' "$archive_paths" |
|
||||
grep -E '(^superpowers/|^\.agents/|^hooks/|package\.json$|^\.git|^\.pytest_cache|^\.ruff_cache|^scripts/|^tests/|^docs/|^evals/|^lib/|^\.claude|^\.cursor|^\.kimi|^\.opencode|^\.pi|^AGENTS\.md$|^CLAUDE\.md$|^GEMINI\.md$|^RELEASE-NOTES\.md$|^CHANGELOG\.md$)' || true
|
||||
)"
|
||||
if [[ -n "$unexpected_paths" ]]; then
|
||||
printf '%s\n' "$unexpected_paths" | sed 's/^/ /' >&2
|
||||
die "archive contains source-only paths"
|
||||
fi
|
||||
|
||||
entry_count="$(printf '%s\n' "$archive_paths" | wc -l | tr -d ' ')"
|
||||
checksum="$(shasum -a 256 "$OUTPUT" | awk '{print $1}')"
|
||||
|
||||
echo "Archive: $OUTPUT"
|
||||
echo "Format: $FORMAT"
|
||||
echo "Version: $VERSION"
|
||||
echo "Entries: $entry_count"
|
||||
echo "Skills: $skill_count"
|
||||
echo "SHA-256: $checksum"
|
||||
Executable
+466
@@ -0,0 +1,466 @@
|
||||
#!/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/), preserves
|
||||
# OpenAI-owned marketplace metadata already in the destination plugin, 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"
|
||||
"/.gitmodules"
|
||||
"/.kimi-plugin/"
|
||||
"/.opencode/"
|
||||
"/.pi/"
|
||||
"/.pre-commit-config.yaml"
|
||||
"/.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/"
|
||||
"/evals/"
|
||||
"/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"
|
||||
SYNC_SOURCE=""
|
||||
|
||||
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[@]}" "$SYNC_SOURCE/" "$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
|
||||
|
||||
copy_preserved_destination_metadata() {
|
||||
local destination="$1"
|
||||
local source="$2"
|
||||
local path
|
||||
local rel
|
||||
|
||||
[[ -d "$destination/skills" ]] || return 0
|
||||
|
||||
while IFS= read -r -d '' path; do
|
||||
rel="${path#"$destination"/}"
|
||||
mkdir -p "$source/$(dirname "$rel")"
|
||||
cp -p "$path" "$source/$rel"
|
||||
done < <(find "$destination/skills" -path '*/agents/openai.yaml' -type f -print0)
|
||||
}
|
||||
|
||||
prepare_sync_source() {
|
||||
local destination="$1"
|
||||
|
||||
[[ -n "$CLEANUP_DIR" ]] || CLEANUP_DIR="$(mktemp -d)"
|
||||
|
||||
SYNC_SOURCE="$CLEANUP_DIR/source-overlay"
|
||||
rm -rf "$SYNC_SOURCE"
|
||||
mkdir -p "$SYNC_SOURCE"
|
||||
|
||||
rsync "${RSYNC_ARGS[@]}" "$UPSTREAM/" "$SYNC_SOURCE/" >/dev/null
|
||||
copy_preserved_destination_metadata "$destination" "$SYNC_SOURCE"
|
||||
}
|
||||
|
||||
prepare_sync_source "$PREVIEW_DEST"
|
||||
|
||||
# =============================================================================
|
||||
# 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 "$SYNC_SOURCE/" "$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[@]}" "$SYNC_SOURCE/" "$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\`, \`assets/\`, and \`hooks/\`.
|
||||
|
||||
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, assets, and hooks.
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user