Files
gstack/gstack-upgrade/migrations/v1.0.1.0.sh
Garry Tan 229d9a6505 merge: origin/main v1.0.0.0 into garrytan/fix-checkpoints
Main shipped the v1 prompts rewrite (simpler writing style + real LOC
receipts + /plan-tune observational substrate). Resolved conflicts:

- VERSION / package.json: bumped 0.18.5.0 → 1.0.1.0 (main is 1.0.0.0,
  this branch lands next).
- CHANGELOG: moved the /context-save + /context-restore entry to the
  top as v1.0.1.0, above main's v1.0.0.0. Also removed the em-dash
  variants in the new entry (ship voice rule).
- TODOS: kept both sections — Context skills (lane feature TODO) first,
  main's PACING_UPDATES_V0 + Plan Tune v2 deferrals below.
- Migration: renamed gstack-upgrade/migrations/v0.18.5.0.sh →
  v1.0.1.0.sh (matches new version). Test path updated.

preamble.ts auto-merged cleanly: main's question-tuning, explain_level,
and writing-style sections composed with my context-save/context-restore
routing rule.

All SKILL.md files regenerated via `bun run gen:skill-docs --host all`
per CLAUDE.md's "never resolve generated files by accepting either
side" rule. Golden fixtures (claude/codex/factory ship) also regenerated.

bun test: 0 failures.
2026-04-18 17:24:03 +08:00

105 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Migration: v1.0.1.0 — Remove stale /checkpoint skill installs
#
# Claude Code ships /checkpoint as a native alias for /rewind, which was
# shadowing the gstack checkpoint skill. The skill has been split into
# /context-save + /context-restore. This migration removes the old on-disk
# install so Claude Code's native /checkpoint is no longer shadowed.
#
# Ownership guard: the script only removes the install IF it owns it —
# i.e., the directory or its SKILL.md is a symlink resolving inside
# ~/.claude/skills/gstack/. A user's own /checkpoint skill (regular file,
# or symlink pointing elsewhere) is preserved.
#
# Three supported install shapes to handle:
# 1. ~/.claude/skills/checkpoint is a directory symlink into gstack.
# 2. ~/.claude/skills/checkpoint is a regular directory whose ONLY file
# is a SKILL.md symlink into gstack (gstack's prefix-install shape).
# 3. Anything else → leave alone, print notice.
#
# Idempotent: missing paths are no-ops.
set -euo pipefail
SKILLS_DIR="${HOME}/.claude/skills"
OLD_TOPLEVEL="${SKILLS_DIR}/checkpoint"
OLD_NAMESPACED="${SKILLS_DIR}/gstack/checkpoint"
GSTACK_ROOT_REAL=""
# Resolve the canonical path of the gstack skills root. If gstack isn't
# installed here, there's nothing to migrate.
if [ -d "${SKILLS_DIR}/gstack" ]; then
# Portable realpath: macOS BSD `readlink` lacks -f. Fall back to python3.
if command -v realpath >/dev/null 2>&1; then
GSTACK_ROOT_REAL=$(realpath "${SKILLS_DIR}/gstack" 2>/dev/null || true)
fi
if [ -z "$GSTACK_ROOT_REAL" ]; then
GSTACK_ROOT_REAL=$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "${SKILLS_DIR}/gstack" 2>/dev/null || true)
fi
fi
# Helper: canonical-path a target (symlink-safe). Prints the resolved path.
resolve_real() {
local target="$1"
if command -v realpath >/dev/null 2>&1; then
realpath "$target" 2>/dev/null || true
return
fi
python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "$target" 2>/dev/null || true
}
# Helper: does $1 (canonical path) live inside $2 (canonical path)?
path_inside() {
local inner="$1"
local outer="$2"
[ -n "$inner" ] && [ -n "$outer" ] || return 1
case "$inner" in
"$outer"|"$outer"/*) return 0;;
*) return 1;;
esac
}
removed_any=0
# --- Shape 1: top-level ~/.claude/skills/checkpoint
if [ -L "$OLD_TOPLEVEL" ]; then
# Directory symlink (or file symlink). Canonicalize and check ownership.
target_real=$(resolve_real "$OLD_TOPLEVEL")
if [ -n "$GSTACK_ROOT_REAL" ] && path_inside "$target_real" "$GSTACK_ROOT_REAL"; then
rm "$OLD_TOPLEVEL"
echo " [v1.0.1.0] Removed stale /checkpoint symlink (was shadowing Claude Code's /rewind alias)."
removed_any=1
else
echo " [v1.0.1.0] Leaving $OLD_TOPLEVEL alone — symlink target is outside gstack."
fi
elif [ -d "$OLD_TOPLEVEL" ]; then
# Regular directory. Only remove if it contains exactly one file named
# SKILL.md that's a symlink into gstack (gstack's prefix-install shape).
entries=$(ls -A "$OLD_TOPLEVEL" 2>/dev/null)
if [ "$entries" = "SKILL.md" ] && [ -L "$OLD_TOPLEVEL/SKILL.md" ]; then
target_real=$(resolve_real "$OLD_TOPLEVEL/SKILL.md")
if [ -n "$GSTACK_ROOT_REAL" ] && path_inside "$target_real" "$GSTACK_ROOT_REAL"; then
rm -r "$OLD_TOPLEVEL"
echo " [v1.0.1.0] Removed stale /checkpoint install directory (gstack prefix-mode)."
removed_any=1
else
echo " [v1.0.1.0] Leaving $OLD_TOPLEVEL alone — SKILL.md symlink target is outside gstack."
fi
else
echo " [v1.0.1.0] Leaving $OLD_TOPLEVEL alone — not a gstack-owned install (has custom content)."
fi
fi
# Missing → no-op (idempotency).
# --- Shape 2: ~/.claude/skills/gstack/checkpoint/ (gstack owns this dir unconditionally)
if [ -d "$OLD_NAMESPACED" ] || [ -L "$OLD_NAMESPACED" ]; then
rm -rf "$OLD_NAMESPACED"
echo " [v1.0.1.0] Removed stale ~/.claude/skills/gstack/checkpoint/ (replaced by context-save + context-restore)."
removed_any=1
fi
if [ "$removed_any" = "1" ]; then
echo " [v1.0.1.0] /checkpoint is now Claude Code's native /rewind alias. Use /context-save to save state and /context-restore to resume."
fi
exit 0