mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
484cf1fb3b
* refactor: extract processExternalHost() shared helper for multi-host generation Refactor the Codex-specific output routing block in gen-skill-docs.ts into a shared processExternalHost() function. Both Codex and future external hosts (Factory Droid) will use this helper for output routing, symlink loop detection, frontmatter transformation, path rewrites, and metadata generation. - Rename codexSkillName() to externalSkillName() everywhere - Extract ExternalHostConfig interface with per-host settings - Codex output is byte-identical (verified via --dry-run) - Skip /codex skill for all non-Claude hosts (not just codex) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add Factory Droid host type, preamble, and co-author trailer - Add 'factory' to Host union type with .factory/skills/gstack paths - Extend preamble runtime root detection for Factory ($HOME/.factory/) - Add GSTACK_DESIGN env var to preamble (was missing for Codex too) - Add Factory Droid co-author trailer for git commits Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Factory Droid generation, --host all, and host-aware frontmatter - Add --host factory (alias: --host droid) to gen-skill-docs - Add --host all: generates for claude, codex, and factory in one invocation with fault-tolerant per-host error handling (only fails if claude fails) - Factory frontmatter: name + description + user-invocable: true - Factory sensitive skills: disable-model-invocation: true (from sensitive: field) - Claude: strips sensitive: field from output (only Factory uses it) - Factory tool name translation: Claude tool names → generic phrasing - Replace chained gen:skill-docs calls with --host all in package.json build Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: sensitive frontmatter for Factory Droid auto-invocation safety Add sensitive: true to 6 skill templates with side effects that Factory Droids shouldn't auto-invoke (ship, land-and-deploy, guard, careful, freeze, unfreeze). The field is: - Factory: emitted as disable-model-invocation: true - Claude/Codex: stripped from output by transformFrontmatter() Also fix Claude host path: call transformFrontmatter() for Claude to strip the sensitive: field from Claude output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: gstack-platform-detect binary for multi-host debugging Bash script that prints a table of installed AI coding agents (Claude, Codex, Factory Droid, Kiro) with versions, skill paths, and gstack installation status. Useful for debugging multi-host setups. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Factory Droid support in setup script - Add factory to --host values (auto-detected via command -v droid) - Add .factory/ skill doc generation step alongside .agents/ - Add create_factory_runtime_root() and link_factory_skill_dirs() helpers mirroring the Codex equivalents - Factory install section creates ~/.factory/skills/ with symlinks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: Factory Droid awareness in skill-check and uninstall - skill-check.ts: add Factory skills validation and freshness check - gstack-uninstall: add Factory artifact cleanup (~/.factory/skills/gstack* and per-project .factory/ sidecar) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: Factory Droid generation + --host all test suites Add 13 new tests: - Factory output paths, frontmatter (user-invocable, disable-model-invocation) - Sensitive vs non-sensitive skill classification - Path rewrites (no .claude/skills/ in Factory output) - /codex skill exclusion, openai.yaml absence - Factory keeps Codex integration blocks (for second opinions) - --host droid alias, --dry-run freshness, preamble paths - --host all generates for all 3 hosts - Setup script host validation updated for factory Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: Factory Droid install instructions + CI freshness check - README: add Factory Droid section with install instructions and restart note (Factory requires restart to rescan skills) - CI: add Factory skill doc freshness verification to skill-docs.yml Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: generated Factory Droid skill output (.factory/skills/) 29 skills generated for Factory Droid with: - user-invocable: true on all skills - disable-model-invocation: true on 6 sensitive skills - .factory/skills/ paths (no .claude/skills/ references) - $GSTACK_ROOT env vars for runtime root detection - Tool name translation (Claude tool names → generic phrasing) Committed to git for CI freshness checks and direct consumption. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: add Factory Droid P1 TODO for browse MCP server Add 3 TODOs under new ## Factory Droid section: - P1: Browse MCP server (Option B, deeper Factory integration) - P3: .agent/skills/ dual output for cross-agent compatibility - P3: Custom Droid definitions alongside skills Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.13.5.0) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
253 lines
10 KiB
Bash
Executable File
253 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-uninstall — remove gstack skills, state, and browse daemons
|
|
#
|
|
# Usage:
|
|
# gstack-uninstall — interactive uninstall (prompts before removing)
|
|
# gstack-uninstall --force — remove everything without prompting
|
|
# gstack-uninstall --keep-state — remove skills but keep ~/.gstack/ data
|
|
#
|
|
# What gets REMOVED:
|
|
# ~/.claude/skills/gstack — global Claude skill install (git clone or vendored)
|
|
# ~/.claude/skills/{skill} — per-skill symlinks created by setup
|
|
# ~/.codex/skills/gstack* — Codex skill install + per-skill symlinks
|
|
# ~/.factory/skills/gstack* — Factory Droid skill install + per-skill symlinks
|
|
# ~/.kiro/skills/gstack* — Kiro skill install + per-skill symlinks
|
|
# ~/.gstack/ — global state (config, analytics, sessions, projects,
|
|
# repos, installation-id, browse error logs)
|
|
# .claude/skills/gstack* — project-local skill install (--local installs)
|
|
# .gstack/ — per-project browse state (in current git repo)
|
|
# .gstack-worktrees/ — per-project test worktrees (in current git repo)
|
|
# .agents/skills/gstack* — Codex/Gemini/Cursor sidecar (in current git repo)
|
|
# Running browse daemons — stopped via SIGTERM before cleanup
|
|
#
|
|
# What is NOT REMOVED:
|
|
# ~/Library/Caches/ms-playwright/ — Playwright Chromium (shared, may be used by other tools)
|
|
# ~/.gstack-dev/ — developer eval artifacts (only present in gstack contributors)
|
|
#
|
|
# Env overrides (for testing):
|
|
# GSTACK_DIR — override auto-detected gstack root
|
|
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
|
#
|
|
# NOTE: Uses set -uo pipefail (no -e) — uninstall must never abort partway.
|
|
set -uo pipefail
|
|
|
|
if [ -z "${HOME:-}" ]; then
|
|
echo "ERROR: \$HOME is not set" >&2
|
|
exit 1
|
|
fi
|
|
|
|
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
|
_GIT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
|
|
# ─── Parse flags ─────────────────────────────────────────────
|
|
FORCE=0
|
|
KEEP_STATE=0
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--force) FORCE=1; shift ;;
|
|
--keep-state) KEEP_STATE=1; shift ;;
|
|
-h|--help)
|
|
sed -n '2,/^[^#]/{ /^#/s/^# \{0,1\}//p; }' "$0"
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
echo "Usage: gstack-uninstall [--force] [--keep-state]" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ─── Confirmation ────────────────────────────────────────────
|
|
if [ "$FORCE" -eq 0 ]; then
|
|
echo "This will remove gstack from your system:"
|
|
{ [ -d "$HOME/.claude/skills/gstack" ] || [ -L "$HOME/.claude/skills/gstack" ]; } && echo " ~/.claude/skills/gstack (+ per-skill symlinks)"
|
|
[ -d "$HOME/.codex/skills" ] && echo " ~/.codex/skills/gstack*"
|
|
[ -d "$HOME/.factory/skills" ] && echo " ~/.factory/skills/gstack*"
|
|
[ -d "$HOME/.kiro/skills" ] && echo " ~/.kiro/skills/gstack*"
|
|
[ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ] && echo " $STATE_DIR"
|
|
|
|
if [ -n "$_GIT_ROOT" ]; then
|
|
[ -d "$_GIT_ROOT/.claude/skills/gstack" ] && echo " $_GIT_ROOT/.claude/skills/gstack (project-local)"
|
|
[ -d "$_GIT_ROOT/.gstack" ] && echo " $_GIT_ROOT/.gstack/ (browse state + reports)"
|
|
[ -d "$_GIT_ROOT/.gstack-worktrees" ] && echo " $_GIT_ROOT/.gstack-worktrees/"
|
|
[ -d "$_GIT_ROOT/.agents/skills" ] && echo " $_GIT_ROOT/.agents/skills/gstack*"
|
|
fi
|
|
|
|
# Preview running daemons
|
|
if [ -n "$_GIT_ROOT" ] && [ -f "$_GIT_ROOT/.gstack/browse.json" ]; then
|
|
_PREVIEW_PID="$(awk -F'[:,]' '/"pid"/ { for(i=1;i<=NF;i++) if($i ~ /"pid"/) { gsub(/[^0-9]/, "", $(i+1)); print $(i+1); exit } }' "$_GIT_ROOT/.gstack/browse.json" 2>/dev/null || true)"
|
|
[ -n "$_PREVIEW_PID" ] && kill -0 "$_PREVIEW_PID" 2>/dev/null && echo " browse daemon (PID $_PREVIEW_PID) will be stopped"
|
|
fi
|
|
|
|
printf "\nContinue? [y/N] "
|
|
read -r REPLY
|
|
case "$REPLY" in
|
|
y|Y|yes|YES) ;;
|
|
*) echo "Aborted."; exit 0 ;;
|
|
esac
|
|
fi
|
|
|
|
REMOVED=()
|
|
|
|
# ─── Stop running browse daemons ─────────────────────────────
|
|
# Browse servers write PID to {project}/.gstack/browse.json.
|
|
# Stop any we can find before removing state directories.
|
|
stop_browse_daemon() {
|
|
local state_file="$1"
|
|
if [ ! -f "$state_file" ]; then
|
|
return
|
|
fi
|
|
local pid
|
|
pid="$(awk -F'[:,]' '/"pid"/ { for(i=1;i<=NF;i++) if($i ~ /"pid"/) { gsub(/[^0-9]/, "", $(i+1)); print $(i+1); exit } }' "$state_file" 2>/dev/null || true)"
|
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
kill "$pid" 2>/dev/null || true
|
|
# Wait up to 2s for graceful shutdown
|
|
local waited=0
|
|
while [ "$waited" -lt 4 ] && kill -0 "$pid" 2>/dev/null; do
|
|
sleep 0.5
|
|
waited=$(( waited + 1 ))
|
|
done
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
kill -9 "$pid" 2>/dev/null || true
|
|
fi
|
|
REMOVED+=("browse daemon (PID $pid)")
|
|
fi
|
|
}
|
|
|
|
# Stop daemon in current project
|
|
if [ -n "$_GIT_ROOT" ] && [ -f "$_GIT_ROOT/.gstack/browse.json" ]; then
|
|
stop_browse_daemon "$_GIT_ROOT/.gstack/browse.json"
|
|
fi
|
|
|
|
# Stop daemons tracked in global projects directory
|
|
if [ -d "$STATE_DIR/projects" ]; then
|
|
while IFS= read -r _BJ; do
|
|
stop_browse_daemon "$_BJ"
|
|
done < <(find "$STATE_DIR/projects" -name browse.json -path '*/.gstack/*' 2>/dev/null || true)
|
|
fi
|
|
|
|
# ─── Remove global Claude skills ────────────────────────────
|
|
CLAUDE_SKILLS="$HOME/.claude/skills"
|
|
if [ -d "$CLAUDE_SKILLS/gstack" ] || [ -L "$CLAUDE_SKILLS/gstack" ]; then
|
|
# Remove per-skill symlinks that point into gstack/
|
|
for _LINK in "$CLAUDE_SKILLS"/*; do
|
|
[ -L "$_LINK" ] || continue
|
|
_NAME="$(basename "$_LINK")"
|
|
[ "$_NAME" = "gstack" ] && continue
|
|
_TARGET="$(readlink "$_LINK" 2>/dev/null || true)"
|
|
case "$_TARGET" in
|
|
gstack/*|*/gstack/*) rm -f "$_LINK"; REMOVED+=("claude/$_NAME") ;;
|
|
esac
|
|
done
|
|
|
|
rm -rf "$CLAUDE_SKILLS/gstack"
|
|
REMOVED+=("~/.claude/skills/gstack")
|
|
fi
|
|
|
|
# ─── Remove project-local Claude skills (--local installs) ──
|
|
if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.claude/skills" ]; then
|
|
for _LINK in "$_GIT_ROOT/.claude/skills"/*; do
|
|
[ -L "$_LINK" ] || continue
|
|
_TARGET="$(readlink "$_LINK" 2>/dev/null || true)"
|
|
case "$_TARGET" in
|
|
gstack/*|*/gstack/*) rm -f "$_LINK"; REMOVED+=("local claude/$(basename "$_LINK")") ;;
|
|
esac
|
|
done
|
|
if [ -d "$_GIT_ROOT/.claude/skills/gstack" ] || [ -L "$_GIT_ROOT/.claude/skills/gstack" ]; then
|
|
rm -rf "$_GIT_ROOT/.claude/skills/gstack"
|
|
REMOVED+=("$_GIT_ROOT/.claude/skills/gstack")
|
|
fi
|
|
fi
|
|
|
|
# ─── Remove Codex skills ────────────────────────────────────
|
|
CODEX_SKILLS="$HOME/.codex/skills"
|
|
if [ -d "$CODEX_SKILLS" ]; then
|
|
for _ITEM in "$CODEX_SKILLS"/gstack*; do
|
|
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
|
|
rm -rf "$_ITEM"
|
|
REMOVED+=("codex/$(basename "$_ITEM")")
|
|
done
|
|
fi
|
|
|
|
# ─── Remove Factory Droid skills ────────────────────────────
|
|
FACTORY_SKILLS="$HOME/.factory/skills"
|
|
if [ -d "$FACTORY_SKILLS" ]; then
|
|
for _ITEM in "$FACTORY_SKILLS"/gstack*; do
|
|
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
|
|
rm -rf "$_ITEM"
|
|
REMOVED+=("factory/$(basename "$_ITEM")")
|
|
done
|
|
fi
|
|
|
|
# ─── Remove Kiro skills ─────────────────────────────────────
|
|
KIRO_SKILLS="$HOME/.kiro/skills"
|
|
if [ -d "$KIRO_SKILLS" ]; then
|
|
for _ITEM in "$KIRO_SKILLS"/gstack*; do
|
|
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
|
|
rm -rf "$_ITEM"
|
|
REMOVED+=("kiro/$(basename "$_ITEM")")
|
|
done
|
|
fi
|
|
|
|
# ─── Remove per-project .agents/ sidecar ─────────────────────
|
|
if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.agents/skills" ]; then
|
|
for _ITEM in "$_GIT_ROOT/.agents/skills"/gstack*; do
|
|
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
|
|
rm -rf "$_ITEM"
|
|
REMOVED+=("agents/$(basename "$_ITEM")")
|
|
done
|
|
|
|
rmdir "$_GIT_ROOT/.agents/skills" 2>/dev/null || true
|
|
rmdir "$_GIT_ROOT/.agents" 2>/dev/null || true
|
|
fi
|
|
|
|
# ─── Remove per-project .factory/ sidecar ────────────────────
|
|
if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.factory/skills" ]; then
|
|
for _ITEM in "$_GIT_ROOT/.factory/skills"/gstack*; do
|
|
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
|
|
rm -rf "$_ITEM"
|
|
REMOVED+=("factory/$(basename "$_ITEM")")
|
|
done
|
|
|
|
rmdir "$_GIT_ROOT/.factory/skills" 2>/dev/null || true
|
|
rmdir "$_GIT_ROOT/.factory" 2>/dev/null || true
|
|
fi
|
|
|
|
# ─── Remove per-project state ───────────────────────────────
|
|
if [ -n "$_GIT_ROOT" ]; then
|
|
if [ -d "$_GIT_ROOT/.gstack" ]; then
|
|
rm -rf "$_GIT_ROOT/.gstack"
|
|
REMOVED+=("$_GIT_ROOT/.gstack/")
|
|
fi
|
|
if [ -d "$_GIT_ROOT/.gstack-worktrees" ]; then
|
|
rm -rf "$_GIT_ROOT/.gstack-worktrees"
|
|
REMOVED+=("$_GIT_ROOT/.gstack-worktrees/")
|
|
fi
|
|
fi
|
|
|
|
# ─── Remove global state ────────────────────────────────────
|
|
if [ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ]; then
|
|
rm -rf "$STATE_DIR"
|
|
REMOVED+=("$STATE_DIR")
|
|
fi
|
|
|
|
# ─── Clean up temp files ────────────────────────────────────
|
|
for _TMP in /tmp/gstack-latest-version /tmp/gstack-sketch-*.html /tmp/gstack-sketch.png /tmp/gstack-sync-*; do
|
|
if [ -e "$_TMP" ]; then
|
|
rm -f "$_TMP"
|
|
REMOVED+=("$(basename "$_TMP")")
|
|
fi
|
|
done
|
|
|
|
# ─── Summary ────────────────────────────────────────────────
|
|
if [ ${#REMOVED[@]} -gt 0 ]; then
|
|
echo "Removed: ${REMOVED[*]}"
|
|
echo "gstack uninstalled."
|
|
else
|
|
echo "Nothing to remove — gstack is not installed."
|
|
fi
|
|
|
|
exit 0
|