mirror of
https://github.com/Dimillian/Skills.git
synced 2026-03-25 08:55:54 +00:00
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:
parent
08c8616b05
commit
26e23c058d
5 changed files with 249 additions and 100 deletions
|
|
@ -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
|
||||
|
|
|
|||
168
docs/app.js
168
docs/app.js
|
|
@ -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
85
docs/skills.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
87
scripts/build_docs_index.py
Normal file
87
scripts/build_docs_index.py
Normal 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()
|
||||
6
scripts/git-hooks/pre-commit
Normal file
6
scripts/git-hooks/pre-commit
Normal 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"
|
||||
Loading…
Reference in a new issue