feat: migrate 12 personal skills + add build/install scripts

- skills/: editable sources for all 12 personal skills (caveman family,
  find-skills, project-bootstrap, task-status-wiki, using-context7,
  using-markitdown, wiki-maintainer, compress)
- dist/: built .skill archives, committed (cross-machine deploy without
  needing zip on the target)
- scripts/build.sh + build.ps1: zip skills/<name>/ into dist/<name>.skill;
  Windows fallback uses .NET ZipArchive (PS 5.1 Compress-Archive writes
  backslashes that break extraction on *nix)
- scripts/install.sh: copy skills/<name>/ into ~/.claude/skills/<name>/
  ($CLAUDE_SKILLS_DIR overrides target)
- .wiki/source/repo-layout.md, build-notes.md: design + gotchas
- .gitignore: keep dist/ tracked

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 10:42:33 +03:00
parent 782a00d6b8
commit 9f54068de0
51 changed files with 3243 additions and 6 deletions

71
scripts/build.ps1 Normal file
View File

@@ -0,0 +1,71 @@
# Build .skill archives from skills/<name>/ into dist/<name>.skill
# Usage: build.ps1 [-Names <name1>,<name2>] (no args = all)
#
# Uses .NET System.IO.Compression.ZipArchive directly to produce
# spec-compliant ZIPs with forward-slash entry names (Windows PowerShell 5.1's
# Compress-Archive writes backslashes, which break extraction on Linux/Mac).
[CmdletBinding()]
param(
[string[]]$Names = @()
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$src = Join-Path $root 'skills'
$dist = Join-Path $root 'dist'
if (-not (Test-Path $dist)) { New-Item -Type Directory -Path $dist | Out-Null }
if ($Names.Count -eq 0) {
$Names = Get-ChildItem -Path $src -Directory | Sort-Object Name | Select-Object -ExpandProperty Name
}
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
function New-SkillArchive {
param(
[string]$SkillName,
[string]$SourceDir,
[string]$OutPath
)
if (Test-Path $OutPath) { Remove-Item $OutPath -Force }
$sourceFull = (Resolve-Path $SourceDir).ProviderPath.TrimEnd('\','/')
$archive = [System.IO.Compression.ZipFile]::Open(
$OutPath,
[System.IO.Compression.ZipArchiveMode]::Create
)
try {
$files = Get-ChildItem -Path $sourceFull -Recurse -File
foreach ($file in $files) {
$rel = $file.FullName.Substring($sourceFull.Length + 1) -replace '\\','/'
$entryName = "$SkillName/$rel"
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
$archive,
$file.FullName,
$entryName,
[System.IO.Compression.CompressionLevel]::Optimal
) | Out-Null
}
} finally {
$archive.Dispose()
}
}
foreach ($name in $Names) {
$srcDir = Join-Path $src $name
if (-not (Test-Path $srcDir -PathType Container)) {
Write-Warning "skip: $name (not found in skills/)"
continue
}
if (-not (Test-Path (Join-Path $srcDir 'SKILL.md'))) {
Write-Warning "skip: $name (missing SKILL.md)"
continue
}
$out = Join-Path $dist "$name.skill"
New-SkillArchive -SkillName $name -SourceDir $srcDir -OutPath $out
Write-Host "built: dist/$name.skill"
}

51
scripts/build.sh Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Build .skill archives from skills/<name>/ into dist/<name>.skill
# Usage: build.sh [name...] (no args = all)
#
# Uses `zip` if available; otherwise delegates to scripts/build.ps1
# (so the script works on Linux, macOS, and Windows-with-git-bash without
# requiring `zip` on Windows).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
SRC="$ROOT/skills"
DIST="$ROOT/dist"
if command -v zip >/dev/null 2>&1; then
mkdir -p "$DIST"
if [ "$#" -eq 0 ]; then
mapfile -t names < <(find "$SRC" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
else
names=("$@")
fi
for name in "${names[@]}"; do
src_dir="$SRC/$name"
if [ ! -d "$src_dir" ]; then
echo "skip: $name (not found in skills/)" >&2
continue
fi
if [ ! -f "$src_dir/SKILL.md" ]; then
echo "skip: $name (missing SKILL.md)" >&2
continue
fi
out="$DIST/$name.skill"
rm -f "$out"
( cd "$SRC" && zip -qr "$out" "$name" )
echo "built: dist/$name.skill"
done
elif command -v powershell.exe >/dev/null 2>&1; then
# Windows fallback: delegate to build.ps1 (proper ZIP via .NET API).
ps1="$SCRIPT_DIR/build.ps1"
if [ "$#" -eq 0 ]; then
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$ps1" 2>/dev/null || echo "$ps1")"
else
# Build a comma-separated PS argument list.
joined=$(printf ",'%s'" "$@")
joined="${joined:1}"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(cygpath -w "$ps1" 2>/dev/null || echo "$ps1")" -Names "$joined"
fi
else
echo "error: need either 'zip' (Linux/macOS) or PowerShell (Windows) to build .skill archives" >&2
exit 1
fi

32
scripts/install.sh Normal file
View File

@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Install skills/<name>/ into ~/.claude/skills/<name>/ (or $CLAUDE_SKILLS_DIR)
# Usage: install.sh [name...] (no args = all)
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SRC="$ROOT/skills"
TARGET="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}"
if [ "$#" -eq 0 ]; then
mapfile -t names < <(find "$SRC" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort)
else
names=("$@")
fi
mkdir -p "$TARGET"
for name in "${names[@]}"; do
src_dir="$SRC/$name"
dst_dir="$TARGET/$name"
if [ ! -d "$src_dir" ]; then
echo "skip: $name (not found in skills/)" >&2
continue
fi
if [ ! -f "$src_dir/SKILL.md" ]; then
echo "skip: $name (missing SKILL.md)" >&2
continue
fi
rm -rf "$dst_dir"
cp -R "$src_dir" "$dst_dir"
echo "installed: $name$dst_dir"
done