Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion bin/gstack-uninstall
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,11 @@ fi
# ─── Remove SessionStart hook from Claude Code settings ─────
SETTINGS_HOOK="$(dirname "$0")/gstack-settings-hook"
SESSION_UPDATE="$(dirname "$0")/gstack-session-update"
SETTINGS_FILE="${GSTACK_SETTINGS_FILE:-$HOME/.claude/settings.json}"
if [ -x "$SETTINGS_HOOK" ]; then
"$SETTINGS_HOOK" remove "$SESSION_UPDATE" 2>/dev/null && REMOVED+=("SessionStart hook") || true
if [ -f "$SETTINGS_FILE" ] && grep -q 'gstack-session-update' "$SETTINGS_FILE" 2>/dev/null; then
"$SETTINGS_HOOK" remove "$SESSION_UPDATE" 2>/dev/null && REMOVED+=("SessionStart hook") || true
fi
fi

# ─── Remove global state ────────────────────────────────────
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gstack",
"version": "0.15.15.0",
"version": "0.15.16.0",
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
"license": "MIT",
"type": "module",
Expand Down
24 changes: 17 additions & 7 deletions setup
Original file line number Diff line number Diff line change
Expand Up @@ -442,11 +442,16 @@ link_codex_skill_dirs() {
# symlink that Step 5 already pointed at the repo root.
[ "$skill_name" = "gstack" ] && continue
target="$skills_dir/$skill_name"
# Create or update symlink
if [ -L "$target" ] || [ ! -e "$target" ]; then
ln -snf "$skill_dir" "$target"
linked+=("$skill_name")
# Upgrade stale real directories from prior installs to symlinks.
if [ -L "$target" ]; then
current_target="$(readlink "$target" 2>/dev/null || true)"
[ "$current_target" = "$skill_dir" ] && continue
rm -f "$target"
elif [ -e "$target" ]; then
rm -rf "$target"
fi
ln -snf "$skill_dir" "$target"
linked+=("$skill_name")
fi
done
if [ ${#linked[@]} -gt 0 ]; then
Expand Down Expand Up @@ -591,10 +596,15 @@ link_factory_skill_dirs() {
skill_name="$(basename "$skill_dir")"
[ "$skill_name" = "gstack" ] && continue
target="$skills_dir/$skill_name"
if [ -L "$target" ] || [ ! -e "$target" ]; then
ln -snf "$skill_dir" "$target"
linked+=("$skill_name")
if [ -L "$target" ]; then
current_target="$(readlink "$target" 2>/dev/null || true)"
[ "$current_target" = "$skill_dir" ] && continue
rm -f "$target"
elif [ -e "$target" ]; then
rm -rf "$target"
fi
ln -snf "$skill_dir" "$target"
linked+=("$skill_name")
fi
done
if [ ${#linked[@]} -gt 0 ]; then
Expand Down
18 changes: 18 additions & 0 deletions test/gen-skill-docs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2076,6 +2076,15 @@ describe('setup script validation', () => {
expect(fnBody).toContain('gstack*');
});

test('link_codex_skill_dirs replaces stale real directories from prior installs', () => {
const fnStart = setupContent.indexOf('link_codex_skill_dirs()');
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart));
const fnBody = setupContent.slice(fnStart, fnEnd);
expect(fnBody).toContain('current_target="$(readlink "$target"');
expect(fnBody).toContain('rm -rf "$target"');
expect(fnBody).toContain('ln -snf "$skill_dir" "$target"');
});

test('link_claude_skill_dirs creates real directories with absolute SKILL.md symlinks', () => {
// Claude links should be real directories with absolute SKILL.md symlinks
// to ensure Claude Code discovers them as top-level skills (not nested under gstack/)
Expand Down Expand Up @@ -2184,6 +2193,15 @@ describe('setup script validation', () => {
expect(setupContent).toContain('avoid duplicate skill discovery');
});

test('link_factory_skill_dirs also replaces stale real directories', () => {
const fnStart = setupContent.indexOf('link_factory_skill_dirs()');
const fnEnd = setupContent.indexOf('}', setupContent.indexOf('linked[@]}', fnStart));
const fnBody = setupContent.slice(fnStart, fnEnd);
expect(fnBody).toContain('current_target="$(readlink "$target"');
expect(fnBody).toContain('rm -rf "$target"');
expect(fnBody).toContain('ln -snf "$skill_dir" "$target"');
});

// --- Symlink prefix tests (PR #503) ---

test('link_claude_skill_dirs applies gstack- prefix by default', () => {
Expand Down