Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/validate-skills.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Validate Skills

on:
pull_request:
push:

jobs:
validate:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Validate skill catalog
run: bash ./scripts/validate_skills.sh
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.DS_Store
.worktrees/
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Portable Elixir and Phoenix skills for Codex.

This repository packages custom skills from a local Codex setup so they can be installed on any machine into `~/.codex/skills`.

Each install writes a manifest of the skills this repository manages. Future installs use that manifest to prune skills that were removed from the repo without touching unrelated skills in the target directory.

## Included

- Elixir/Phoenix skills under [`skills/`](skills/)
Expand Down Expand Up @@ -44,6 +46,8 @@ git pull
./install.sh
```

If a previously installed skill has been removed from this repository, a subsequent install prunes it automatically by consulting the stored manifest.

## Uninstall

```bash
Expand All @@ -62,6 +66,14 @@ Custom target:
CODEX_HOME=/path/to/.codex ./uninstall.sh
```

## Validate

```bash
bash ./scripts/validate_skills.sh
```

This checks the skill catalog for broken local `references/...` links and verifies the install and uninstall scripts, including the guarantee that `install.sh --dry-run` leaves the filesystem untouched.

## Acknowledgment

Inspired by [oliver-kriska/claude-elixir-phoenix](https://github.com/oliver-kriska/claude-elixir-phoenix). This repository is a Codex-oriented adaptation and is not an official fork.
106 changes: 88 additions & 18 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,51 @@ EOF

DRY_RUN=0

case "${1:-}" in
--dry-run)
DRY_RUN=1
;;
"" )
;;
-h|--help)
usage
exit 0
;;
*)
usage
exit 1
;;
esac
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=1
;;
-h|--help)
usage
exit 0
;;
*)
usage
exit 1
;;
esac

shift
done

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_DIR="$REPO_ROOT/skills"
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
TARGET_DIR="$CODEX_HOME/skills"

mkdir -p "$TARGET_DIR"
MANIFEST_PATH="$TARGET_DIR/.codex-elixir-phoenix-manifest"

echo "Source: $SOURCE_DIR"
echo "Target: $TARGET_DIR"

list_source_skills() {
find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d | sort
}

skill_in_list() {
local needle="$1"
shift
local skill_name

for skill_name in "$@"; do
if [[ "$skill_name" == "$needle" ]]; then
return 0
fi
done

return 1
}

copy_skill() {
local skill_name="$1"
local source_path="$SOURCE_DIR/$skill_name"
Expand All @@ -54,15 +73,66 @@ copy_skill() {
echo "installed $skill_name"
}

prune_removed_skills() {
local skill_name
local target_path

if [[ ! -f "$MANIFEST_PATH" ]]; then
return
fi

while IFS= read -r skill_name; do
[[ -z "$skill_name" ]] && continue

if skill_in_list "$skill_name" "${SOURCE_SKILLS[@]}"; then
continue
fi

target_path="$TARGET_DIR/$skill_name"

if [[ "$DRY_RUN" -eq 1 ]]; then
echo "[dry-run] prune $target_path"
continue
fi

if [[ -d "$target_path" ]]; then
rm -rf "$target_path"
echo "pruned $skill_name"
fi
done < "$MANIFEST_PATH"
}

write_manifest() {
if [[ "$DRY_RUN" -eq 1 ]]; then
return
fi

printf '%s\n' "${SOURCE_SKILLS[@]}" > "$MANIFEST_PATH"
}

SOURCE_SKILLS=()

while IFS= read -r skill_path; do
skill_name="$(basename "$skill_path")"

if [[ "$skill_name" == ".system" ]]; then
continue
fi

SOURCE_SKILLS+=("$skill_name")
done < <(list_source_skills)

if [[ "$DRY_RUN" -ne 1 ]]; then
mkdir -p "$TARGET_DIR"
fi

prune_removed_skills

for skill_name in "${SOURCE_SKILLS[@]}"; do
copy_skill "$skill_name"
done < <(find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d | sort)
done

write_manifest

if [[ "$DRY_RUN" -eq 1 ]]; then
echo "Dry run complete."
Expand Down
120 changes: 120 additions & 0 deletions scripts/validate_skills.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env bash

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SKILLS_DIR="$REPO_ROOT/skills"
TEMP_DIRS=()
ERRORS=0

cleanup() {
local dir

for dir in "${TEMP_DIRS[@]}"; do
if [[ -d "$dir" ]]; then
rm -rf "$dir"
fi
done
}

trap cleanup EXIT

make_temp_dir() {
local dir

dir="$(mktemp -d "${TMPDIR:-/tmp}/codex-elixir-phoenix.XXXXXX")"
TEMP_DIRS+=("$dir")
printf '%s\n' "$dir"
}

record_error() {
echo "ERROR: $*" >&2
ERRORS=$((ERRORS + 1))
}

list_source_skills() {
find "$SKILLS_DIR" -mindepth 1 -maxdepth 1 -type d | sort
}

validate_skill_references() {
local skill_dir
local skill_file
local ref

while IFS= read -r skill_dir; do
skill_file="$skill_dir/SKILL.md"

if [[ ! -f "$skill_file" ]]; then
record_error "$(basename "$skill_dir") is missing SKILL.md"
continue
fi

while IFS= read -r ref; do
[[ -z "$ref" ]] && continue

if [[ ! -f "$skill_dir/$ref" ]]; then
record_error "$(basename "$skill_dir") references missing file: $ref"
fi
done < <(rg -o 'references/[A-Za-z0-9._/-]+' "$skill_file" | sort -u || true)
done < <(list_source_skills)
}

validate_dry_run_is_clean() {
local sandbox

sandbox="$(make_temp_dir)"
CODEX_HOME="$sandbox" "$REPO_ROOT/install.sh" --dry-run >/dev/null

if [[ -e "$sandbox/skills" ]]; then
record_error "install.sh --dry-run created $sandbox/skills"
fi
}

validate_install_round_trip() {
local sandbox
local manifest_path
local skill_dir
local skill_name

sandbox="$(make_temp_dir)"
manifest_path="$sandbox/skills/.codex-elixir-phoenix-manifest"

CODEX_HOME="$sandbox" "$REPO_ROOT/install.sh" >/dev/null

if [[ ! -f "$manifest_path" ]]; then
record_error "install.sh did not write the ownership manifest"
fi

while IFS= read -r skill_dir; do
skill_name="$(basename "$skill_dir")"

if [[ ! -d "$sandbox/skills/$skill_name" ]]; then
record_error "install.sh did not install $skill_name"
fi
done < <(list_source_skills)

CODEX_HOME="$sandbox" "$REPO_ROOT/uninstall.sh" >/dev/null

if [[ -f "$manifest_path" ]]; then
record_error "uninstall.sh did not remove the ownership manifest"
fi

while IFS= read -r skill_dir; do
skill_name="$(basename "$skill_dir")"

if [[ -e "$sandbox/skills/$skill_name" ]]; then
record_error "uninstall.sh did not remove $skill_name"
fi
done < <(list_source_skills)
}

validate_skill_references
validate_dry_run_is_clean
validate_install_round_trip

if [[ "$ERRORS" -gt 0 ]]; then
echo "Validation failed with $ERRORS error(s)." >&2
exit 1
fi

echo "Skill catalog validation passed."
6 changes: 3 additions & 3 deletions skills/elixir-phoenix-compound/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ existing** (same root cause, new symptom), or **Skip**.
Extract from session context: module, symptoms, investigation
steps, root cause, solution code, and prevention advice.

Validate frontmatter against `compound-docs/references/schema.md`,
then create file using `compound-docs/references/resolution-template.md`.
Open `elixir-phoenix-compound-docs` and use its schema and
resolution template references before writing the solution file.

### Step 4: Decision Menu

Expand Down Expand Up @@ -86,4 +86,4 @@ When user says "that worked", "it's fixed", "problem solved",
## References

- `references/compound-workflow.md` — Detailed step-by-step
- See also: `compound-docs` skill for schema and templates
- See also: `elixir-phoenix-compound-docs` for schema and templates
Loading
Loading