Skip to content

Commit 8cefa04

Browse files
committed
feat: gstack-skill-install — one-command install from GitHub
Clone → validate → copy .tmpl → ready to use. Integrates with gstack-skill-validate for security gating. Usage: gstack-skill-install owner/repo # install gstack-skill-install owner/repo --dry-run # validate only gstack-skill-install --list # show community skills gstack-skill-install --remove skill-name # uninstall Tracks installations in ~/.gstack/community-skills/ for management.
1 parent 3501f5d commit 8cefa04

1 file changed

Lines changed: 136 additions & 0 deletions

File tree

bin/gstack-skill-install

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env bash
2+
# gstack-skill-install — install a community skill from GitHub
3+
#
4+
# Clones a skill repo, validates it, copies the .tmpl + supporting files,
5+
# and regenerates SKILL.md. One command from discovery to usage.
6+
#
7+
# Usage:
8+
# gstack-skill-install <owner/repo> # install from GitHub
9+
# gstack-skill-install <owner/repo> --dry-run # validate without installing
10+
# gstack-skill-install --list # list installed community skills
11+
# gstack-skill-install --remove <skill-name> # uninstall a community skill
12+
#
13+
# Flow: clone → validate → copy .tmpl → gen-skill-docs → ready
14+
set -euo pipefail
15+
16+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17+
GSTACK_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
18+
COMMUNITY_DIR="$HOME/.gstack/community-skills"
19+
20+
case "${1:-}" in
21+
--list)
22+
echo "Installed community skills:"
23+
if [ -d "$COMMUNITY_DIR" ]; then
24+
for f in "$COMMUNITY_DIR"/*.json; do
25+
[ -f "$f" ] || continue
26+
python3 -c "
27+
import json; d=json.load(open('$f'))
28+
print(f' /{d[\"name\"]:20s} from {d[\"repo\"]:30s} installed {d[\"installed\"][:10]}')
29+
" 2>/dev/null
30+
done
31+
else
32+
echo " (none)"
33+
fi
34+
exit 0
35+
;;
36+
--remove)
37+
SKILL="${2:?Usage: gstack-skill-install --remove <skill-name>}"
38+
if [ -d "$GSTACK_DIR/$SKILL" ] && [ -f "$COMMUNITY_DIR/$SKILL.json" ]; then
39+
rm -rf "$GSTACK_DIR/$SKILL"
40+
rm -f "$COMMUNITY_DIR/$SKILL.json"
41+
echo "Removed: /$SKILL"
42+
echo "Run: bun run gen:skill-docs"
43+
else
44+
echo "Not a community skill: $SKILL"
45+
exit 1
46+
fi
47+
exit 0
48+
;;
49+
--help|-h)
50+
echo "Usage: gstack-skill-install <owner/repo> [--dry-run]"
51+
exit 0
52+
;;
53+
esac
54+
55+
REPO="${1:?Usage: gstack-skill-install <owner/repo>}"
56+
DRY_RUN=""
57+
[ "${2:-}" = "--dry-run" ] && DRY_RUN=1
58+
59+
# Clone to temp dir
60+
TMPDIR=$(mktemp -d)
61+
trap 'rm -rf "$TMPDIR"' EXIT
62+
63+
echo "Fetching $REPO..."
64+
gh repo clone "$REPO" "$TMPDIR/skill" -- --depth 1 --quiet 2>/dev/null
65+
if [ $? -ne 0 ]; then
66+
echo "ERROR: Could not clone $REPO"
67+
exit 1
68+
fi
69+
70+
# Find SKILL.md.tmpl
71+
TMPL=$(find "$TMPDIR/skill" -name "SKILL.md.tmpl" -maxdepth 2 | head -1)
72+
if [ -z "$TMPL" ]; then
73+
echo "ERROR: No SKILL.md.tmpl found in $REPO"
74+
exit 1
75+
fi
76+
77+
SKILL_DIR=$(dirname "$TMPL")
78+
SKILL_NAME=$(python3 -c "
79+
import re
80+
content = open('$TMPL').read()
81+
m = re.search(r'^name:\s*(\S+)', content, re.M)
82+
print(m.group(1) if m else '$(basename "$SKILL_DIR")')
83+
" 2>/dev/null)
84+
85+
echo "Found skill: /$SKILL_NAME"
86+
87+
# Validate
88+
echo "Validating..."
89+
if [ -x "$SCRIPT_DIR/gstack-skill-validate" ]; then
90+
"$SCRIPT_DIR/gstack-skill-validate" "$TMPL"
91+
VALIDATE_EXIT=$?
92+
if [ $VALIDATE_EXIT -eq 1 ]; then
93+
echo ""
94+
echo "BLOCKED: Skill failed security validation. Not installing."
95+
exit 1
96+
fi
97+
else
98+
echo " (gstack-skill-validate not found — skipping validation)"
99+
fi
100+
101+
if [ -n "$DRY_RUN" ]; then
102+
echo ""
103+
echo "DRY RUN: Would install /$SKILL_NAME from $REPO"
104+
echo " Source: $TMPL"
105+
echo " Target: $GSTACK_DIR/$SKILL_NAME/"
106+
exit 0
107+
fi
108+
109+
# Install
110+
TARGET="$GSTACK_DIR/$SKILL_NAME"
111+
mkdir -p "$TARGET"
112+
cp "$TMPL" "$TARGET/"
113+
114+
# Copy supporting files
115+
for f in "$SKILL_DIR"/*.md "$SKILL_DIR"/templates/* "$SKILL_DIR"/references/*; do
116+
[ -f "$f" ] && [ "$(basename "$f")" != "SKILL.md" ] && cp "$f" "$TARGET/" 2>/dev/null || true
117+
done
118+
119+
# Record installation
120+
mkdir -p "$COMMUNITY_DIR"
121+
cat > "$COMMUNITY_DIR/$SKILL_NAME.json" << MEOF
122+
{
123+
"name": "$SKILL_NAME",
124+
"repo": "$REPO",
125+
"installed": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
126+
"gstack_version": "$(cat "$GSTACK_DIR/VERSION" 2>/dev/null || echo "unknown")"
127+
}
128+
MEOF
129+
130+
echo ""
131+
echo "Installed: /$SKILL_NAME"
132+
echo ""
133+
echo "Generate skill docs:"
134+
echo " cd $GSTACK_DIR && bun run gen:skill-docs"
135+
echo ""
136+
echo "Then use: /$SKILL_NAME"

0 commit comments

Comments
 (0)