Add dynamic skills index and pre-commit hook

Replaces hardcoded skills in docs/app.js with dynamic loading from a generated docs/skills.json. Adds scripts/build_docs_index.py to generate the index from SKILL.md files, and a pre-commit hook to keep the index in sync. Updates README with setup instructions for the pre-commit hook.
This commit is contained in:
Thomas Ricouard 2025-12-31 08:51:09 +01:00
parent 08c8616b05
commit 26e23c058d
5 changed files with 249 additions and 100 deletions

View file

@ -10,6 +10,9 @@ This repository contains a set of focused skills designed to assist with common
Install: place these skill folders under `$CODEX_HOME/skills/public` (or symlink this repo there).
Optional: enable the pre-commit hook to keep `docs/skills.json` in sync:
`git config core.hooksPath scripts/git-hooks`
## Skills
### 📝 App Store Changelog

View file

@ -1,90 +1,3 @@
const skills = [
{
name: "App Store Changelog",
folder: "app-store-changelog",
description:
"Generate App Store release notes from git history with user-focused summaries.",
references: [
{
title: "Release notes guidelines",
file: "references/release-notes-guidelines.md",
},
],
},
{
name: "iOS Debugger Agent",
folder: "ios-debugger-agent",
description:
"Build, run, and debug iOS apps on simulators with UI interaction and log capture.",
references: [],
},
{
name: "Swift Concurrency Expert",
folder: "swift-concurrency-expert",
description:
"Review and remediate Swift 6.2+ concurrency issues with actor isolation and Sendable safety.",
references: [
{
title: "Swift 6.2 concurrency",
file: "references/swift-6-2-concurrency.md",
},
{
title: "SwiftUI concurrency tour",
file: "references/swiftui-concurrency-tour-wwdc.md",
},
],
},
{
name: "SwiftUI Liquid Glass",
folder: "swiftui-liquid-glass",
description:
"Adopt and review Liquid Glass APIs in SwiftUI with correct usage patterns and fallbacks.",
references: [
{
title: "Liquid Glass reference",
file: "references/liquid-glass.md",
},
],
},
{
name: "SwiftUI View Refactor",
folder: "swiftui-view-refactor",
description:
"Refactor SwiftUI views for consistent structure, dependency injection, and Observation usage.",
references: [
{
title: "MV patterns",
file: "references/mv-patterns.md",
},
],
},
{
name: "SwiftUI Performance Audit",
folder: "swiftui-performance-audit",
description:
"Code-first review for SwiftUI performance pitfalls with targeted fixes and profiling guidance.",
references: [
{
title: "Optimizing with Instruments",
file: "references/optimizing-swiftui-performance-instruments.md",
},
{
title: "Understanding SwiftUI performance",
file: "references/understanding-improving-swiftui-performance.md",
},
{
title: "Understanding hangs",
file: "references/understanding-hangs-in-your-app.md",
},
{
title: "Demystify SwiftUI performance",
file: "references/demystify-swiftui-performance-wwdc23.md",
},
],
},
];
const repoInfo = getRepoInfo();
const githubLink = document.getElementById("githubLink");
const themeToggle = document.getElementById("themeToggle");
const skillsList = document.getElementById("skillsList");
@ -94,15 +7,48 @@ const skillUsage = document.getElementById("skillUsage");
const referenceBar = document.getElementById("referenceBar");
const markdownContent = document.getElementById("markdownContent");
const repoInfo = getRepoInfo();
githubLink.href = repoInfo
? `https://github.com/${repoInfo.owner}/${repoInfo.repo}`
: "#";
renderSkillList();
selectSkill(skills[0]);
initTheme();
loadSkills();
function renderSkillList() {
function loadSkills() {
fetch("skills.json")
.then((response) => {
if (!response.ok) {
throw new Error("Failed to load skills index");
}
return response.json();
})
.then((skills) => {
if (!Array.isArray(skills) || skills.length === 0) {
throw new Error("No skills found");
}
const normalized = skills.map((skill) => normalizeSkill(skill));
renderSkillList(normalized);
selectSkill(normalized[0], normalized);
})
.catch(() => {
markdownContent.textContent =
"Unable to load the skills index. Run scripts/build_docs_index.py to regenerate docs/skills.json.";
});
}
function normalizeSkill(skill) {
const displayName = prettifyName(skill.name || skill.folder || "Skill");
return {
name: displayName,
rawName: skill.name || displayName,
folder: skill.folder,
description: skill.description || "",
references: skill.references || [],
};
}
function renderSkillList(skills) {
skillsList.innerHTML = "";
skills.forEach((skill) => {
const item = document.createElement("button");
@ -131,17 +77,17 @@ function renderSkillList() {
preview.textContent = truncateText(skill.description, 110);
item.append(title, meta, preview);
item.addEventListener("click", () => selectSkill(skill));
item.addEventListener("click", () => selectSkill(skill, skills));
skillsList.append(item);
});
}
function selectSkill(skill) {
function selectSkill(skill, skills) {
setActiveSkill(skill);
skillTitle.textContent = skill.name;
skillDescription.textContent = skill.description;
skillUsage.textContent = "";
renderReferenceBar(skill);
renderReferenceBar(skill, skills);
loadMarkdown(skill, "SKILL.md");
}
@ -176,7 +122,7 @@ function renderReferenceBar(skill) {
const refButton = document.createElement("button");
refButton.className = "reference-pill";
refButton.type = "button";
refButton.textContent = ref.title;
refButton.textContent = ref.title || prettifyName(ref.file);
refButton.addEventListener("click", () => {
setActiveReference(refButton);
loadMarkdown(skill, ref.file);
@ -227,7 +173,7 @@ function buildContentPath(path) {
function updateHeader(frontmatter, overview, skill) {
if (frontmatter.name) {
skillTitle.textContent = frontmatter.name;
skillTitle.textContent = prettifyName(frontmatter.name);
}
if (frontmatter.description) {
skillDescription.textContent = frontmatter.description;
@ -244,10 +190,10 @@ function parseFrontmatter(text) {
return { frontmatter: {}, content: text };
}
const parts = text.split("\n");
const lines = text.split("\n");
let endIndex = -1;
for (let i = 1; i < parts.length; i += 1) {
if (parts[i].trim() === "---") {
for (let i = 1; i < lines.length; i += 1) {
if (lines[i].trim() === "---") {
endIndex = i;
break;
}
@ -257,8 +203,8 @@ function parseFrontmatter(text) {
return { frontmatter: {}, content: text };
}
const yamlLines = parts.slice(1, endIndex);
const content = parts.slice(endIndex + 1).join("\n");
const yamlLines = lines.slice(1, endIndex);
const content = lines.slice(endIndex + 1).join("\n");
const frontmatter = {};
yamlLines.forEach((line) => {
@ -304,6 +250,28 @@ function truncateText(text, maxLength) {
return `${text.slice(0, maxLength - 1).trim()}`;
}
function prettifyName(name) {
if (!name) return "";
if (/[A-Z]/.test(name) && !name.includes("-")) {
return name;
}
const parts = name.replace(/-/g, " ").split(" ");
const map = {
ios: "iOS",
swiftui: "SwiftUI",
swift: "Swift",
app: "App",
gh: "GitHub",
};
return parts
.map((part) => {
const lower = part.toLowerCase();
if (map[lower]) return map[lower];
return lower.charAt(0).toUpperCase() + lower.slice(1);
})
.join(" ");
}
function getRepoInfo() {
const host = window.location.hostname;
const pathParts = window.location.pathname.split("/").filter(Boolean);

85
docs/skills.json Normal file
View file

@ -0,0 +1,85 @@
[
{
"name": "app-store-changelog",
"folder": "app-store-changelog",
"description": "Create user-facing App Store release notes by collecting and summarizing all user-impacting changes since the last git tag (or a specified ref). Use when asked to generate a comprehensive release changelog, App Store \"What's New\" text, or release notes based on git history or tags.",
"references": [
{
"title": "App Store Release Notes Guidelines",
"file": "references/release-notes-guidelines.md"
}
]
},
{
"name": "gh-issue-fix-flow",
"folder": "gh-issue-fix-flow",
"description": "End-to-end GitHub issue fix workflow using gh, local code changes, builds/tests, and git push. Use when asked to take an issue number, inspect the issue via gh, implement a fix, run XcodeBuildMCP builds/tests, commit with a closing message, and push.",
"references": []
},
{
"name": "ios-debugger-agent",
"folder": "ios-debugger-agent",
"description": "Use XcodeBuildMCP to build, run, launch, and debug the current iOS project on a booted simulator. Trigger when asked to run an iOS app, interact with the simulator UI, inspect on-screen state, capture logs/console output, or diagnose runtime behavior using XcodeBuildMCP tools.",
"references": []
},
{
"name": "swift-concurrency-expert",
"folder": "swift-concurrency-expert",
"description": "Swift Concurrency review and remediation for Swift 6.2+. Use when asked to review Swift Concurrency usage, improve concurrency compliance, or fix Swift concurrency compiler errors in a feature or file.",
"references": [
{
"title": "Swift 6 2 Concurrency",
"file": "references/swift-6-2-concurrency.md"
},
{
"title": "SwiftUI Concurrency Tour (Summary)",
"file": "references/swiftui-concurrency-tour-wwdc.md"
}
]
},
{
"name": "swiftui-liquid-glass",
"folder": "swiftui-liquid-glass",
"description": "Implement, review, or improve SwiftUI features using the iOS 26+ Liquid Glass API. Use when asked to adopt Liquid Glass in new SwiftUI UI, refactor an existing feature to Liquid Glass, or review Liquid Glass usage for correctness, performance, and design alignment.",
"references": [
{
"title": "Implementing Liquid Glass Design in SwiftUI",
"file": "references/liquid-glass.md"
}
]
},
{
"name": "swiftui-performance-audit",
"folder": "swiftui-performance-audit",
"description": "Audit and improve SwiftUI runtime performance from code review and architecture. Use for requests to diagnose slow rendering, janky scrolling, high CPU/memory usage, excessive view updates, or layout thrash in SwiftUI apps, and to provide guidance for user-run Instruments profiling when code review alone is insufficient.",
"references": [
{
"title": "Demystify SwiftUI Performance (WWDC23) (Summary)",
"file": "references/demystify-swiftui-performance-wwdc23.md"
},
{
"title": "Optimizing SwiftUI Performance with Instruments (Summary)",
"file": "references/optimizing-swiftui-performance-instruments.md"
},
{
"title": "Understanding Hangs in Your App (Summary)",
"file": "references/understanding-hangs-in-your-app.md"
},
{
"title": "Understanding and Improving SwiftUI Performance (Summary)",
"file": "references/understanding-improving-swiftui-performance.md"
}
]
},
{
"name": "swiftui-view-refactor",
"folder": "swiftui-view-refactor",
"description": "Refactor and review SwiftUI view files for consistent structure, dependency injection, and Observation usage. Use when asked to clean up a SwiftUI view\u2019s layout/ordering, handle view models safely (non-optional when possible), or standardize how dependencies and @Observable state are initialized and passed.",
"references": [
{
"title": "MV Patterns Reference",
"file": "references/mv-patterns.md"
}
]
}
]

View file

@ -0,0 +1,87 @@
#!/usr/bin/env python3
import json
from pathlib import Path
def parse_frontmatter(text: str) -> tuple[dict, str]:
if not text.startswith("---"):
return {}, text
lines = text.splitlines()
end_index = None
for i in range(1, len(lines)):
if lines[i].strip() == "---":
end_index = i
break
if end_index is None:
return {}, text
frontmatter_lines = lines[1:end_index]
content = "\n".join(lines[end_index + 1 :])
frontmatter = {}
for line in frontmatter_lines:
if ":" not in line:
continue
key, value = line.split(":", 1)
frontmatter[key.strip()] = value.strip().strip('"')
return frontmatter, content
def infer_title_from_markdown(path: Path) -> str:
try:
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("# "):
return line.replace("# ", "", 1).strip()
except FileNotFoundError:
return path.stem.replace("-", " ").title()
return path.stem.replace("-", " ").title()
def build_index(root: Path) -> list[dict]:
skills = []
for skill_dir in sorted(root.iterdir()):
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
continue
frontmatter, _ = parse_frontmatter(skill_md.read_text(encoding="utf-8"))
name = frontmatter.get("name", skill_dir.name)
description = frontmatter.get("description", "").strip()
references = []
references_dir = skill_dir / "references"
if references_dir.exists():
for ref in sorted(references_dir.glob("*.md")):
references.append(
{
"title": infer_title_from_markdown(ref),
"file": f"references/{ref.name}",
}
)
skills.append(
{
"name": name,
"folder": skill_dir.name,
"description": description,
"references": references,
}
)
return skills
def main() -> None:
root = Path(__file__).resolve().parents[1]
output = root / "docs" / "skills.json"
skills = build_index(root)
output.write_text(json.dumps(skills, indent=2), encoding="utf-8")
print(f"Wrote {output}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
python3 "${repo_root}/scripts/build_docs_index.py"
git add "${repo_root}/docs/skills.json"