
Generating Swift Package Docs
- 105 installs
- 222 repo stars
- Updated January 18, 2026
- johnrogers/claude-swift-engineering
Helps with ai & agent building tasks.
About
generating-swift-package-docs is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- generating-swift-package-docs
- AI & Agent Building
- AI-coding skill
Generating Swift Package Docs by the numbers
- 105 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #4,196 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/johnrogers/claude-swift-engineering --skill generating-swift-package-docsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 105 |
|---|---|
| repo stars | ★ 222 |
| Last updated | January 18, 2026 |
| Repository | johnrogers/claude-swift-engineering ↗ |
What it does
Helps with ai & agent building tasks.
Files
Swift Package Documentation Generator
Generates API documentation for Swift package dependencies on-demand, extracting symbol information from Xcode's DerivedData to answer "what does this library do?"
Overview
When exploring unfamiliar dependencies, generate their documentation automatically instead of guessing from code. This tool uses interfazzle to extract symbol information from compiled modules.
How to Use
When asked about an unfamiliar Swift module import:
1. Run: ./scripts/generate_docs.py "<module_name>" "<path_to.xcodeproj>" 2. Script outputs path to cached documentation file 3. Read the file and provide relevant information
Prerequisites: Project must be built once (DerivedData exists), interfazzle CLI installed.
See reference.md for error handling and details.
Swift Package Documentation Generator - Reference
Automatically generates comprehensive API documentation for Swift package dependencies using `interfazzle`.
Contents
Features
- Automatic package resolution: Maps module names to package names using dependency information
- Smart caching: Checks for existing documentation before generating
- Clean integration: Uses OS temporary directories for generation with automatic cleanup
- Comprehensive output: Combines all generated markdown with package README files
- Version-aware: Generates docs with version-specific filenames (major.minor format)
Command-Line Usage
python3 ./scripts/generate_docs.py <module_or_package_name> <xcodeproj_path>Arguments
module_or_package_name: The Swift module or package name (e.g.,ButtonKit,Defaults)xcodeproj_path: Path to the Xcode project file (e.g.,/path/to/MyApp.xcodeproj)
Example
python3 ./scripts/generate_docs.py ButtonKit /Users/yourname/Code/MyProject/MyProject.xcodeprojOutput:
/Users/yourname/Code/MyProject/dependency-docs/ButtonKit-0.6.mdHow It Works
From within Claude Code, this skill automatically:
1. Resolves module to package using shared Swift package utilities 2. Checks for existing documentation in dependency-docs/ 3. If docs don't exist:
- Locates the package in DerivedData
- Extracts version from git tags (major.minor only)
- Runs
interfazzle generatewith OS temporary directory - Concatenates all generated
.mdfiles - Appends the package's README if it exists
- Saves to
dependency-docs/<package-name>-<major.minor>.md - Temporary directory is automatically cleaned up
4. Returns the documentation file path
When to Use
Use this skill when:
- You encounter an unfamiliar module import and need its API documentation
- You want to explore a dependency's API surface
- You need to reference package documentation while coding
- Working with Swift packages and need quick access to their public interfaces
Example Scenario
When you encounter an unfamiliar import:
import ButtonKitThe skill generates (or retrieves) documentation at:
<project>/dependency-docs/ButtonKit-0.6.mdRequirements
- Python 3.6+
interfazzleCLI tool installed and in PATH (https://github.com/czottmann/interfazzle)- Shared Swift package utilities (
_shared/swift_packages.py) - Project must be built at least once (DerivedData must exist)
Output Format
Documentation files are saved as:
<project>/dependency-docs/<PackageName>-<major.minor>.mdThis means:
- Documentation is generated once per major.minor version
- Subsequent requests for the same package version use the cached file
- Patch version updates don't trigger regeneration
- Major or minor version updates will generate new documentation
Implementation
The skill consists of:
SKILL.md- Skill definition with YAML frontmatterreference.md- This detailed reference documentationscripts/generate_docs.py- Main implementation script (relative to skill directory)../_shared/swift_packages.py- Shared Swift package utilities (used by multiple skills)
Testing
Verified working with:
- ButtonKit 0.6.1 →
ButtonKit-0.6.md(21KB) - Defaults 8.2.0 →
Defaults-8.2.md(47KB) - Diagnostics 5.1.0 →
Diagnostics-5.1.md(comprehensive API docs)
All successfully generated, cached on subsequent runs, with automatic temp directory cleanup.
#!/usr/bin/env python3
"""
Generate or retrieve documentation for Swift package dependencies.
"""
import sys
import os
import subprocess
import tempfile
from pathlib import Path
# Import utilities from local directory
scripts_dir = Path(__file__).parent
sys.path.insert(0, str(scripts_dir))
from swift_packages import (
get_all_dependencies,
resolve_module_to_package,
find_derived_data_path,
find_package_directory_in_derived_data
)
def error_exit(message):
"""Print error message and exit."""
print(f"Error: {message}", file=sys.stderr)
sys.exit(1)
def run_command(cmd, cwd=None, capture_output=True):
"""Run a shell command and return output."""
try:
result = subprocess.run(
cmd,
cwd=cwd,
capture_output=capture_output,
text=True,
check=True
)
return result.stdout.strip() if capture_output else None
except subprocess.CalledProcessError as e:
error_exit(f"Command failed: {' '.join(cmd)}\n{e.stderr}")
def get_package_version(package_dir):
"""Extract package version from git tags."""
# Try git describe first
try:
version = run_command(["git", "describe", "--tags"], cwd=package_dir)
except:
# Fall back to latest tag
tags = run_command(["git", "tag", "--sort=-version:refname"], cwd=package_dir)
if not tags:
error_exit(f"No git tags found in {package_dir}")
version = tags.split('\n')[0]
# Parse version to major.minor
version = version.lstrip('v') # Remove 'v' prefix if present
parts = version.split('.')
if len(parts) >= 2:
return f"{parts[0]}.{parts[1]}"
return parts[0]
def find_readme(package_dir):
"""Find README file in package directory."""
for filename in ["README.md", "README.txt", "README.markdown"]:
readme_path = package_dir / filename
if readme_path.exists():
return readme_path
return None
def generate_documentation(package_dir, package_name):
"""Run interfazzle to generate documentation."""
with tempfile.TemporaryDirectory() as temp_output_dir:
# Check if build artifacts already exist
build_db = package_dir / ".build" / "build.db"
cmd = ["interfazzle", "generate", "--output-dir", temp_output_dir]
if build_db.exists():
cmd.append("--generate-only")
# Run interfazzle with absolute temp directory path
run_command(cmd, cwd=package_dir, capture_output=False)
# Concatenate all markdown files
temp_path = Path(temp_output_dir)
markdown_files = sorted(temp_path.glob("**/*.md"))
if not markdown_files:
error_exit(f"No markdown files generated by interfazzle")
content_parts = []
# Add generated docs
for md_file in markdown_files:
with open(md_file, 'r') as f:
content_parts.append(f.read())
# Add README if exists
readme_path = find_readme(package_dir)
if readme_path:
content_parts.append("\n\n---\n\n# Package README\n\n")
with open(readme_path, 'r') as f:
content_parts.append(f.read())
return "\n\n".join(content_parts)
# Temp directory is automatically cleaned up when exiting context
def main():
if len(sys.argv) != 3:
print("Usage: python3 generate_docs.py <module_or_package_name> <xcodeproj_path>")
sys.exit(1)
module_or_package = sys.argv[1]
xcodeproj_path_str = sys.argv[2]
xcodeproj_path = Path(xcodeproj_path_str)
# Validate xcodeproj path
if not xcodeproj_path.exists():
error_exit(f"Xcode project not found: {xcodeproj_path}")
project_dir = xcodeproj_path.parent
docs_dir = project_dir / "dependency-docs"
# Get dependency list and resolve package name
print(f"Resolving package name for: {module_or_package}", file=sys.stderr)
dependencies = get_all_dependencies(xcodeproj_path)
package_name = resolve_module_to_package(module_or_package, dependencies)
if package_name is None:
error_exit(f"Could not find package for module: {module_or_package}")
# Get package info
package_info = dependencies[package_name]
version = package_info.get("version", "unknown")
# Parse version to major.minor
version_parts = version.split('.')
short_version = f"{version_parts[0]}.{version_parts[1]}" if len(version_parts) >= 2 else version_parts[0]
# Check if docs already exist
docs_filename = f"{package_name}-{short_version}.md"
docs_path = docs_dir / docs_filename
if docs_path.exists():
print(f"Documentation already exists: {docs_path}", file=sys.stderr)
print(docs_path)
return
# Find package in DerivedData
print(f"Finding package '{package_name}' in DerivedData...", file=sys.stderr)
project_name = xcodeproj_path.stem
checkouts_dir = find_derived_data_path(project_name)
package_dir = find_package_directory_in_derived_data(checkouts_dir, package_name)
if package_dir is None:
error_exit(f"Package '{package_name}' not found in DerivedData. Has the project been built?")
# Generate documentation
print(f"Generating documentation for {package_name} {short_version}...", file=sys.stderr)
content = generate_documentation(package_dir, package_name)
# Save documentation
docs_dir.mkdir(exist_ok=True)
with open(docs_path, 'w') as f:
f.write(content)
print(f"Documentation generated successfully!", file=sys.stderr)
print(docs_path)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Shared utilities for working with Swift Package Manager dependencies in Xcode projects.
Provides functions for parsing Package.resolved, finding packages in DerivedData,
and extracting package metadata.
"""
import sys
import json
import re
from pathlib import Path
from typing import Dict, List, Optional
def find_package_resolved(xcodeproj_path: Path) -> Path:
"""Find the Package.resolved file in the Xcode project."""
possible_locations = [
xcodeproj_path
/ "project.xcworkspace"
/ "xcshareddata"
/ "swiftpm"
/ "Package.resolved",
xcodeproj_path.parent
/ ".swiftpm"
/ "xcode"
/ "package.xcworkspace"
/ "xcshareddata"
/ "swiftpm"
/ "Package.resolved",
]
for location in possible_locations:
if location.exists():
return location
raise FileNotFoundError("Could not find Package.resolved file")
def find_derived_data_path(project_name: str) -> Path:
"""Find the DerivedData directory for this project."""
derived_data_base = Path.home() / "Library" / "Developer" / "Xcode" / "DerivedData"
if not derived_data_base.exists():
raise FileNotFoundError(
f"DerivedData directory not found at {derived_data_base}"
)
# Xcode replaces spaces with underscores in DerivedData directory names
normalized_project_name = project_name.replace(" ", "_")
# Find directories matching the project name
pattern = f"{normalized_project_name}-*"
matching_dirs = list(derived_data_base.glob(pattern))
if not matching_dirs:
raise FileNotFoundError(
f"No DerivedData directory found for project '{project_name}'"
)
# Use the most recently modified one
derived_data_dir = max(matching_dirs, key=lambda p: p.stat().st_mtime)
source_packages = derived_data_dir / "SourcePackages" / "checkouts"
if not source_packages.exists():
raise FileNotFoundError(
f"SourcePackages/checkouts not found in {derived_data_dir}"
)
return source_packages
def parse_package_resolved(resolved_path: Path) -> List[Dict]:
"""Parse Package.resolved to get list of dependencies."""
with open(resolved_path, "r") as f:
data = json.load(f)
# Handle different Package.resolved formats (version 2 and 3)
if "pins" in data:
return data["pins"]
elif "object" in data and "pins" in data["object"]:
return data["object"]["pins"]
else:
raise ValueError("Unknown Package.resolved format")
def extract_package_info(pin: Dict) -> Dict:
"""Extract package name and repository URL from a pin entry."""
# Handle both v2 and v3 formats
if "package" in pin:
name = pin["package"]
elif "identity" in pin:
name = pin["identity"]
else:
name = "Unknown"
if "repositoryURL" in pin:
url = pin["repositoryURL"]
elif "location" in pin:
url = pin["location"]
else:
url = None
version = pin.get("state", {}).get("version") or pin.get("state", {}).get(
"revision", "unknown"
)
return {"name": name, "url": url, "version": version}
def find_package_swift_in_derived_data(
checkouts_dir: Path, package_identity: str
) -> Optional[Path]:
"""Find Package.swift for a given package in DerivedData checkouts."""
# Try exact match first
package_dir = checkouts_dir / package_identity
if package_dir.exists():
package_swift = package_dir / "Package.swift"
if package_swift.exists():
return package_swift
# Try case-insensitive search
for subdir in checkouts_dir.iterdir():
if subdir.is_dir() and subdir.name.lower() == package_identity.lower():
package_swift = subdir / "Package.swift"
if package_swift.exists():
return package_swift
return None
def find_package_directory_in_derived_data(
checkouts_dir: Path, package_identity: str
) -> Optional[Path]:
"""Find package directory in DerivedData checkouts."""
# Try exact match first
package_dir = checkouts_dir / package_identity
if package_dir.exists():
return package_dir
# Try case-insensitive search
for subdir in checkouts_dir.iterdir():
if subdir.is_dir() and subdir.name.lower() == package_identity.lower():
return subdir
return None
def read_package_swift(package_swift_path: Path, verbose: bool = False) -> Optional[str]:
"""Read Package.swift content from local file."""
try:
with open(package_swift_path, "r", encoding="utf-8") as f:
return f.read()
except Exception as e:
if verbose:
print(f"Warning: Could not read {package_swift_path}: {e}", file=sys.stderr)
return None
def parse_package_name_from_package_swift(content: str) -> Optional[str]:
"""Extract the actual package name from Package.swift content."""
if not content:
return None
# Match Package(name: "PackageName", ...) with DOTALL to handle multi-line
package_name_pattern = r'Package\s*\([^)]*?name\s*:\s*"([^"]+)"'
match = re.search(package_name_pattern, content, re.DOTALL)
if match:
return match.group(1)
return None
def parse_targets_from_package_swift(content: str) -> List[str]:
"""Extract target names from Package.swift content."""
if not content:
return []
targets = []
# Match .target patterns
# .target(name: "TargetName", ...)
target_pattern = r'\.target\s*\(\s*name\s*:\s*"([^"]+)"'
targets.extend(re.findall(target_pattern, content))
# Also match .executableTarget patterns
executable_target_pattern = r'\.executableTarget\s*\(\s*name\s*:\s*"([^"]+)"'
targets.extend(re.findall(executable_target_pattern, content))
# Match .testTarget patterns (optional, but sometimes useful)
# test_target_pattern = r'\.testTarget\s*\(\s*name\s*:\s*"([^"]+)"'
# targets.extend(re.findall(test_target_pattern, content))
return targets
def get_all_dependencies(xcodeproj_path: Path, verbose: bool = False) -> Dict[str, Dict]:
"""
Get all dependencies with their metadata for a given Xcode project.
Returns a dictionary mapping package names to their metadata:
{
"PackageName": {
"version": "1.2.3",
"repo": "https://github.com/...",
"exported_modules": ["Module1", "Module2"]
}
}
"""
# Extract project name from .xcodeproj
project_name = xcodeproj_path.stem
# Find and parse Package.resolved
resolved_path = find_package_resolved(xcodeproj_path)
if verbose:
print(f"Found Package.resolved at: {resolved_path}", file=sys.stderr)
pins = parse_package_resolved(resolved_path)
if verbose:
print(f"Found {len(pins)} dependencies", file=sys.stderr)
# Find DerivedData checkouts directory
try:
checkouts_dir = find_derived_data_path(project_name)
if verbose:
print(f"Found package checkouts at: {checkouts_dir}", file=sys.stderr)
except FileNotFoundError as e:
if verbose:
print(f"Warning: {e}", file=sys.stderr)
print("Will not be able to extract module information.", file=sys.stderr)
checkouts_dir = None
dependencies = {}
for pin in pins:
pkg_info = extract_package_info(pin)
if verbose:
print(f"Processing {pkg_info['name']}...", file=sys.stderr)
targets = []
actual_package_name = pkg_info["name"] # Default to identity
# Try to read Package.swift from DerivedData
if checkouts_dir:
package_swift_path = find_package_swift_in_derived_data(
checkouts_dir, pkg_info["name"]
)
if package_swift_path:
if verbose:
print(
f" Found Package.swift at: {package_swift_path}",
file=sys.stderr,
)
package_swift_content = read_package_swift(package_swift_path, verbose)
# Extract actual package name
parsed_name = parse_package_name_from_package_swift(
package_swift_content
)
if parsed_name:
actual_package_name = parsed_name
if verbose:
print(
f" Package name: {actual_package_name}",
file=sys.stderr,
)
targets = parse_targets_from_package_swift(package_swift_content)
else:
if verbose:
print(f" Package.swift not found in checkouts", file=sys.stderr)
dependencies[actual_package_name] = {
"version": pkg_info["version"],
"repo": pkg_info["url"] if pkg_info["url"] else None,
"exported_modules": sorted(targets) if targets else [],
}
return dependencies
def resolve_module_to_package(
module_or_package: str, dependencies: Dict[str, Dict]
) -> Optional[str]:
"""
Resolve a module name to its package name using dependency information.
Args:
module_or_package: Either a module name or package name
dependencies: Dictionary from get_all_dependencies()
Returns:
Package name if found, None otherwise
"""
# Check if it's already a package name
if module_or_package in dependencies:
return module_or_package
# Search for module in exported_modules
for package_name, info in dependencies.items():
if module_or_package in info.get("exported_modules", []):
return package_name
return None