Initial import from garrytan/gstack@026751e (main snapshot via local relay)
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled
Some checks failed
Workflow Lint / actionlint (push) Has been cancelled
Build CI Image / build (push) Has been cancelled
Skill Docs Freshness / check-freshness (push) Has been cancelled
Periodic Evals / build-image (push) Has been cancelled
Periodic Evals / evals (map[file:test/codex-e2e.test.ts name:e2e-codex]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/gemini-e2e.test.ts name:e2e-gemini]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-design.test.ts name:e2e-design]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-plan.test.ts name:e2e-plan]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-bugs.test.ts name:e2e-qa-bugs]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-qa-workflow.test.ts name:e2e-qa-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-review.test.ts name:e2e-review]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-e2e-workflow.test.ts name:e2e-workflow]) (push) Has been cancelled
Periodic Evals / evals (map[file:test/skill-routing-e2e.test.ts name:e2e-routing]) (push) Has been cancelled
Source: https://github.com/garrytan/gstack/commit/026751e
This commit is contained in:
279
gstack-upgrade/SKILL.md
Normal file
279
gstack-upgrade/SKILL.md
Normal file
@@ -0,0 +1,279 @@
|
||||
---
|
||||
name: gstack-upgrade
|
||||
version: 1.1.0
|
||||
description: |
|
||||
Upgrade gstack to the latest version. Detects global vs vendored install,
|
||||
runs the upgrade, and shows what's new. Use when asked to "upgrade gstack",
|
||||
"update gstack", or "get latest version".
|
||||
Voice triggers (speech-to-text aliases): "upgrade the tools", "update the tools", "gee stack upgrade", "g stack upgrade".
|
||||
triggers:
|
||||
- upgrade gstack
|
||||
- update gstack version
|
||||
- get latest gstack
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- AskUserQuestion
|
||||
---
|
||||
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
|
||||
# /gstack-upgrade
|
||||
|
||||
Upgrade gstack to the latest version and show what's new.
|
||||
|
||||
## Inline upgrade flow
|
||||
|
||||
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
|
||||
|
||||
### Step 1: Ask the user (or auto-upgrade)
|
||||
|
||||
First, check if auto-upgrade is enabled:
|
||||
```bash
|
||||
_AUTO=""
|
||||
[ "${GSTACK_AUTO_UPGRADE:-}" = "1" ] && _AUTO="true"
|
||||
[ -z "$_AUTO" ] && _AUTO=$(~/.claude/skills/gstack/bin/gstack-config get auto_upgrade 2>/dev/null || true)
|
||||
echo "AUTO_UPGRADE=$_AUTO"
|
||||
```
|
||||
|
||||
**If `AUTO_UPGRADE=true` or `AUTO_UPGRADE=1`:** Skip AskUserQuestion. Log "Auto-upgrading gstack v{old} → v{new}..." and proceed directly to Step 2. If `./setup` fails during auto-upgrade, restore from backup (`.bak` directory) and warn the user: "Auto-upgrade failed — restored previous version. Run `/gstack-upgrade` manually to retry."
|
||||
|
||||
**Otherwise**, use AskUserQuestion:
|
||||
- Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now?"
|
||||
- Options: ["Yes, upgrade now", "Always keep me up to date", "Not now", "Never ask again"]
|
||||
|
||||
**If "Yes, upgrade now":** Proceed to Step 2.
|
||||
|
||||
**If "Always keep me up to date":**
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-config set auto_upgrade true
|
||||
```
|
||||
Tell user: "Auto-upgrade enabled. Future updates will install automatically." Then proceed to Step 2.
|
||||
|
||||
**If "Not now":** Write snooze state with escalating backoff (first snooze = 24h, second = 48h, third+ = 1 week), then continue with the current skill. Do not mention the upgrade again.
|
||||
```bash
|
||||
_SNOOZE_FILE="$HOME/.gstack/update-snoozed"
|
||||
_REMOTE_VER="{new}"
|
||||
_CUR_LEVEL=0
|
||||
if [ -f "$_SNOOZE_FILE" ]; then
|
||||
_SNOOZED_VER=$(awk '{print $1}' "$_SNOOZE_FILE")
|
||||
if [ "$_SNOOZED_VER" = "$_REMOTE_VER" ]; then
|
||||
_CUR_LEVEL=$(awk '{print $2}' "$_SNOOZE_FILE")
|
||||
case "$_CUR_LEVEL" in *[!0-9]*) _CUR_LEVEL=0 ;; esac
|
||||
fi
|
||||
fi
|
||||
_NEW_LEVEL=$((_CUR_LEVEL + 1))
|
||||
[ "$_NEW_LEVEL" -gt 3 ] && _NEW_LEVEL=3
|
||||
echo "$_REMOTE_VER $_NEW_LEVEL $(date +%s)" > "$_SNOOZE_FILE"
|
||||
```
|
||||
Note: `{new}` is the remote version from the `UPGRADE_AVAILABLE` output — substitute it from the update check result.
|
||||
|
||||
Tell user the snooze duration: "Next reminder in 24h" (or 48h or 1 week, depending on level). Tip: "Set `auto_upgrade: true` in `~/.gstack/config.yaml` for automatic upgrades."
|
||||
|
||||
**If "Never ask again":**
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-config set update_check false
|
||||
```
|
||||
Tell user: "Update checks disabled. Run `~/.claude/skills/gstack/bin/gstack-config set update_check true` to re-enable."
|
||||
Continue with the current skill.
|
||||
|
||||
### Step 2: Detect install type
|
||||
|
||||
```bash
|
||||
if [ -d "$HOME/.claude/skills/gstack/.git" ]; then
|
||||
INSTALL_TYPE="global-git"
|
||||
INSTALL_DIR="$HOME/.claude/skills/gstack"
|
||||
elif [ -d "$HOME/.gstack/repos/gstack/.git" ]; then
|
||||
INSTALL_TYPE="global-git"
|
||||
INSTALL_DIR="$HOME/.gstack/repos/gstack"
|
||||
elif [ -d ".claude/skills/gstack/.git" ]; then
|
||||
INSTALL_TYPE="local-git"
|
||||
INSTALL_DIR=".claude/skills/gstack"
|
||||
elif [ -d ".agents/skills/gstack/.git" ]; then
|
||||
INSTALL_TYPE="local-git"
|
||||
INSTALL_DIR=".agents/skills/gstack"
|
||||
elif [ -d ".claude/skills/gstack" ]; then
|
||||
INSTALL_TYPE="vendored"
|
||||
INSTALL_DIR=".claude/skills/gstack"
|
||||
elif [ -d "$HOME/.claude/skills/gstack" ]; then
|
||||
INSTALL_TYPE="vendored-global"
|
||||
INSTALL_DIR="$HOME/.claude/skills/gstack"
|
||||
else
|
||||
echo "ERROR: gstack not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Install type: $INSTALL_TYPE at $INSTALL_DIR"
|
||||
```
|
||||
|
||||
The install type and directory path printed above will be used in all subsequent steps.
|
||||
|
||||
### Step 3: Save old version
|
||||
|
||||
Use the install directory from Step 2's output below:
|
||||
|
||||
```bash
|
||||
OLD_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
||||
```
|
||||
|
||||
### Step 4: Upgrade
|
||||
|
||||
Use the install type and directory detected in Step 2:
|
||||
|
||||
**For git installs** (global-git, local-git):
|
||||
```bash
|
||||
cd "$INSTALL_DIR"
|
||||
STASH_OUTPUT=$(git stash 2>&1)
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
./setup
|
||||
```
|
||||
If `$STASH_OUTPUT` contains "Saved working directory", warn the user: "Note: local changes were stashed. Run `git stash pop` in the skill directory to restore them."
|
||||
|
||||
**For vendored installs** (vendored, vendored-global):
|
||||
```bash
|
||||
PARENT=$(dirname "$INSTALL_DIR")
|
||||
TMP_DIR=$(mktemp -d)
|
||||
git clone --depth 1 https://github.com/garrytan/gstack.git "$TMP_DIR/gstack"
|
||||
mv "$INSTALL_DIR" "$INSTALL_DIR.bak"
|
||||
mv "$TMP_DIR/gstack" "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR" && ./setup
|
||||
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
|
||||
```
|
||||
|
||||
### Step 4.5: Handle local vendored copy
|
||||
|
||||
Use the install directory from Step 2. Check if there's also a local vendored copy, and whether team mode is active:
|
||||
|
||||
```bash
|
||||
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
LOCAL_GSTACK=""
|
||||
if [ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ]; then
|
||||
_RESOLVED_LOCAL=$(cd "$_ROOT/.claude/skills/gstack" && pwd -P)
|
||||
_RESOLVED_PRIMARY=$(cd "$INSTALL_DIR" && pwd -P)
|
||||
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
|
||||
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
|
||||
fi
|
||||
fi
|
||||
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
|
||||
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
|
||||
echo "TEAM_MODE=$_TEAM_MODE"
|
||||
```
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy. Team mode uses the global install as the single source of truth.
|
||||
|
||||
```bash
|
||||
cd "$_ROOT"
|
||||
git rm -r --cached .claude/skills/gstack/ 2>/dev/null || true
|
||||
if ! grep -qF '.claude/skills/gstack/' .gitignore 2>/dev/null; then
|
||||
echo '.claude/skills/gstack/' >> .gitignore
|
||||
fi
|
||||
rm -rf "$LOCAL_GSTACK"
|
||||
```
|
||||
Tell user: "Removed vendored copy at `$LOCAL_GSTACK` (team mode active — global install is the source of truth). Commit the `.gitignore` change when ready."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:** Update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
|
||||
```bash
|
||||
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
|
||||
cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK"
|
||||
rm -rf "$LOCAL_GSTACK/.git"
|
||||
cd "$LOCAL_GSTACK" && ./setup
|
||||
rm -rf "$LOCAL_GSTACK.bak"
|
||||
```
|
||||
Tell user: "Also updated vendored copy at `$LOCAL_GSTACK` — commit `.claude/skills/gstack/` when you're ready."
|
||||
|
||||
If `./setup` fails, restore from backup and warn the user:
|
||||
```bash
|
||||
rm -rf "$LOCAL_GSTACK"
|
||||
mv "$LOCAL_GSTACK.bak" "$LOCAL_GSTACK"
|
||||
```
|
||||
Tell user: "Sync failed — restored previous version at `$LOCAL_GSTACK`. Run `/gstack-upgrade` manually to retry."
|
||||
|
||||
### Step 4.75: Run version migrations
|
||||
|
||||
After `./setup` completes, run any migration scripts for versions between the old
|
||||
and new version. Migrations handle state fixes that `./setup` alone can't cover
|
||||
(stale config, orphaned files, directory structure changes).
|
||||
|
||||
```bash
|
||||
MIGRATIONS_DIR="$INSTALL_DIR/gstack-upgrade/migrations"
|
||||
if [ -d "$MIGRATIONS_DIR" ]; then
|
||||
for migration in $(find "$MIGRATIONS_DIR" -maxdepth 1 -name 'v*.sh' -type f 2>/dev/null | sort -V); do
|
||||
# Extract version from filename: v0.15.2.0.sh → 0.15.2.0
|
||||
m_ver="$(basename "$migration" .sh | sed 's/^v//')"
|
||||
# Run if this migration version is newer than old version
|
||||
# (simple string compare works for dotted versions with same segment count)
|
||||
if [ "$OLD_VERSION" != "unknown" ] && [ "$(printf '%s\n%s' "$OLD_VERSION" "$m_ver" | sort -V | head -1)" = "$OLD_VERSION" ] && [ "$OLD_VERSION" != "$m_ver" ]; then
|
||||
echo "Running migration $m_ver..."
|
||||
bash "$migration" || echo " Warning: migration $m_ver had errors (non-fatal)"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
```
|
||||
|
||||
Migrations are idempotent bash scripts in `gstack-upgrade/migrations/`. Each is named
|
||||
`v{VERSION}.sh` and runs only when upgrading from an older version. See CONTRIBUTING.md
|
||||
for how to add new migrations.
|
||||
|
||||
### Step 5: Write marker + clear cache
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.gstack
|
||||
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
|
||||
rm -f ~/.gstack/last-update-check
|
||||
rm -f ~/.gstack/update-snoozed
|
||||
```
|
||||
|
||||
### Step 6: Show What's New
|
||||
|
||||
Read `$INSTALL_DIR/CHANGELOG.md`. Find all version entries between the old version and the new version. Summarize as 5-7 bullets grouped by theme. Don't overwhelm — focus on user-facing changes. Skip internal refactors unless they're significant.
|
||||
|
||||
Format:
|
||||
```
|
||||
gstack v{new} — upgraded from v{old}!
|
||||
|
||||
What's new:
|
||||
- [bullet 1]
|
||||
- [bullet 2]
|
||||
- ...
|
||||
|
||||
Happy shipping!
|
||||
```
|
||||
|
||||
### Step 7: Continue
|
||||
|
||||
After showing What's New, continue with whatever skill the user originally invoked. The upgrade is done — no further action needed.
|
||||
|
||||
---
|
||||
|
||||
## Standalone usage
|
||||
|
||||
When invoked directly as `/gstack-upgrade` (not from a preamble):
|
||||
|
||||
1. Force a fresh update check (bypass cache):
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \
|
||||
.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true
|
||||
```
|
||||
Use the output to determine if an upgrade is available.
|
||||
|
||||
2. If `UPGRADE_AVAILABLE <old> <new>`: follow Steps 2-6 above.
|
||||
|
||||
3. If no output (primary is up to date): check for a stale local vendored copy.
|
||||
|
||||
Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`) and team mode status (`TEAM_MODE`).
|
||||
|
||||
**If `LOCAL_GSTACK` is empty** (no local vendored copy): tell the user "You're already on the latest version (v{version})."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy using the Step 4.5 team-mode removal bash block above. Tell user: "Global v{version} is up to date. Removed stale vendored copy (team mode active). Commit the `.gitignore` change when ready."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`**, compare versions:
|
||||
```bash
|
||||
PRIMARY_VER=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
||||
LOCAL_VER=$(cat "$LOCAL_GSTACK/VERSION" 2>/dev/null || echo "unknown")
|
||||
echo "PRIMARY=$PRIMARY_VER LOCAL=$LOCAL_VER"
|
||||
```
|
||||
|
||||
**If versions differ:** follow the Step 4.5 sync bash block above to update the local copy from the primary. Tell user: "Global v{PRIMARY_VER} is up to date. Updated local vendored copy from v{LOCAL_VER} → v{PRIMARY_VER}. Commit `.claude/skills/gstack/` when you're ready."
|
||||
|
||||
**If versions match:** tell the user "You're on the latest version (v{PRIMARY_VER}). Global and local vendored copy are both up to date."
|
||||
281
gstack-upgrade/SKILL.md.tmpl
Normal file
281
gstack-upgrade/SKILL.md.tmpl
Normal file
@@ -0,0 +1,281 @@
|
||||
---
|
||||
name: gstack-upgrade
|
||||
version: 1.1.0
|
||||
description: |
|
||||
Upgrade gstack to the latest version. Detects global vs vendored install,
|
||||
runs the upgrade, and shows what's new. Use when asked to "upgrade gstack",
|
||||
"update gstack", or "get latest version".
|
||||
voice-triggers:
|
||||
- "upgrade the tools"
|
||||
- "update the tools"
|
||||
- "gee stack upgrade"
|
||||
- "g stack upgrade"
|
||||
triggers:
|
||||
- upgrade gstack
|
||||
- update gstack version
|
||||
- get latest gstack
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
# /gstack-upgrade
|
||||
|
||||
Upgrade gstack to the latest version and show what's new.
|
||||
|
||||
## Inline upgrade flow
|
||||
|
||||
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
|
||||
|
||||
### Step 1: Ask the user (or auto-upgrade)
|
||||
|
||||
First, check if auto-upgrade is enabled:
|
||||
```bash
|
||||
_AUTO=""
|
||||
[ "${GSTACK_AUTO_UPGRADE:-}" = "1" ] && _AUTO="true"
|
||||
[ -z "$_AUTO" ] && _AUTO=$(~/.claude/skills/gstack/bin/gstack-config get auto_upgrade 2>/dev/null || true)
|
||||
echo "AUTO_UPGRADE=$_AUTO"
|
||||
```
|
||||
|
||||
**If `AUTO_UPGRADE=true` or `AUTO_UPGRADE=1`:** Skip AskUserQuestion. Log "Auto-upgrading gstack v{old} → v{new}..." and proceed directly to Step 2. If `./setup` fails during auto-upgrade, restore from backup (`.bak` directory) and warn the user: "Auto-upgrade failed — restored previous version. Run `/gstack-upgrade` manually to retry."
|
||||
|
||||
**Otherwise**, use AskUserQuestion:
|
||||
- Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now?"
|
||||
- Options: ["Yes, upgrade now", "Always keep me up to date", "Not now", "Never ask again"]
|
||||
|
||||
**If "Yes, upgrade now":** Proceed to Step 2.
|
||||
|
||||
**If "Always keep me up to date":**
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-config set auto_upgrade true
|
||||
```
|
||||
Tell user: "Auto-upgrade enabled. Future updates will install automatically." Then proceed to Step 2.
|
||||
|
||||
**If "Not now":** Write snooze state with escalating backoff (first snooze = 24h, second = 48h, third+ = 1 week), then continue with the current skill. Do not mention the upgrade again.
|
||||
```bash
|
||||
_SNOOZE_FILE="$HOME/.gstack/update-snoozed"
|
||||
_REMOTE_VER="{new}"
|
||||
_CUR_LEVEL=0
|
||||
if [ -f "$_SNOOZE_FILE" ]; then
|
||||
_SNOOZED_VER=$(awk '{print $1}' "$_SNOOZE_FILE")
|
||||
if [ "$_SNOOZED_VER" = "$_REMOTE_VER" ]; then
|
||||
_CUR_LEVEL=$(awk '{print $2}' "$_SNOOZE_FILE")
|
||||
case "$_CUR_LEVEL" in *[!0-9]*) _CUR_LEVEL=0 ;; esac
|
||||
fi
|
||||
fi
|
||||
_NEW_LEVEL=$((_CUR_LEVEL + 1))
|
||||
[ "$_NEW_LEVEL" -gt 3 ] && _NEW_LEVEL=3
|
||||
echo "$_REMOTE_VER $_NEW_LEVEL $(date +%s)" > "$_SNOOZE_FILE"
|
||||
```
|
||||
Note: `{new}` is the remote version from the `UPGRADE_AVAILABLE` output — substitute it from the update check result.
|
||||
|
||||
Tell user the snooze duration: "Next reminder in 24h" (or 48h or 1 week, depending on level). Tip: "Set `auto_upgrade: true` in `~/.gstack/config.yaml` for automatic upgrades."
|
||||
|
||||
**If "Never ask again":**
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-config set update_check false
|
||||
```
|
||||
Tell user: "Update checks disabled. Run `~/.claude/skills/gstack/bin/gstack-config set update_check true` to re-enable."
|
||||
Continue with the current skill.
|
||||
|
||||
### Step 2: Detect install type
|
||||
|
||||
```bash
|
||||
if [ -d "$HOME/.claude/skills/gstack/.git" ]; then
|
||||
INSTALL_TYPE="global-git"
|
||||
INSTALL_DIR="$HOME/.claude/skills/gstack"
|
||||
elif [ -d "$HOME/.gstack/repos/gstack/.git" ]; then
|
||||
INSTALL_TYPE="global-git"
|
||||
INSTALL_DIR="$HOME/.gstack/repos/gstack"
|
||||
elif [ -d ".claude/skills/gstack/.git" ]; then
|
||||
INSTALL_TYPE="local-git"
|
||||
INSTALL_DIR=".claude/skills/gstack"
|
||||
elif [ -d ".agents/skills/gstack/.git" ]; then
|
||||
INSTALL_TYPE="local-git"
|
||||
INSTALL_DIR=".agents/skills/gstack"
|
||||
elif [ -d ".claude/skills/gstack" ]; then
|
||||
INSTALL_TYPE="vendored"
|
||||
INSTALL_DIR=".claude/skills/gstack"
|
||||
elif [ -d "$HOME/.claude/skills/gstack" ]; then
|
||||
INSTALL_TYPE="vendored-global"
|
||||
INSTALL_DIR="$HOME/.claude/skills/gstack"
|
||||
else
|
||||
echo "ERROR: gstack not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Install type: $INSTALL_TYPE at $INSTALL_DIR"
|
||||
```
|
||||
|
||||
The install type and directory path printed above will be used in all subsequent steps.
|
||||
|
||||
### Step 3: Save old version
|
||||
|
||||
Use the install directory from Step 2's output below:
|
||||
|
||||
```bash
|
||||
OLD_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
||||
```
|
||||
|
||||
### Step 4: Upgrade
|
||||
|
||||
Use the install type and directory detected in Step 2:
|
||||
|
||||
**For git installs** (global-git, local-git):
|
||||
```bash
|
||||
cd "$INSTALL_DIR"
|
||||
STASH_OUTPUT=$(git stash 2>&1)
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
./setup
|
||||
```
|
||||
If `$STASH_OUTPUT` contains "Saved working directory", warn the user: "Note: local changes were stashed. Run `git stash pop` in the skill directory to restore them."
|
||||
|
||||
**For vendored installs** (vendored, vendored-global):
|
||||
```bash
|
||||
PARENT=$(dirname "$INSTALL_DIR")
|
||||
TMP_DIR=$(mktemp -d)
|
||||
git clone --depth 1 https://github.com/garrytan/gstack.git "$TMP_DIR/gstack"
|
||||
mv "$INSTALL_DIR" "$INSTALL_DIR.bak"
|
||||
mv "$TMP_DIR/gstack" "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR" && ./setup
|
||||
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
|
||||
```
|
||||
|
||||
### Step 4.5: Handle local vendored copy
|
||||
|
||||
Use the install directory from Step 2. Check if there's also a local vendored copy, and whether team mode is active:
|
||||
|
||||
```bash
|
||||
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
LOCAL_GSTACK=""
|
||||
if [ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ]; then
|
||||
_RESOLVED_LOCAL=$(cd "$_ROOT/.claude/skills/gstack" && pwd -P)
|
||||
_RESOLVED_PRIMARY=$(cd "$INSTALL_DIR" && pwd -P)
|
||||
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
|
||||
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
|
||||
fi
|
||||
fi
|
||||
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
|
||||
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
|
||||
echo "TEAM_MODE=$_TEAM_MODE"
|
||||
```
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy. Team mode uses the global install as the single source of truth.
|
||||
|
||||
```bash
|
||||
cd "$_ROOT"
|
||||
git rm -r --cached .claude/skills/gstack/ 2>/dev/null || true
|
||||
if ! grep -qF '.claude/skills/gstack/' .gitignore 2>/dev/null; then
|
||||
echo '.claude/skills/gstack/' >> .gitignore
|
||||
fi
|
||||
rm -rf "$LOCAL_GSTACK"
|
||||
```
|
||||
Tell user: "Removed vendored copy at `$LOCAL_GSTACK` (team mode active — global install is the source of truth). Commit the `.gitignore` change when ready."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:** Update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
|
||||
```bash
|
||||
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
|
||||
cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK"
|
||||
rm -rf "$LOCAL_GSTACK/.git"
|
||||
cd "$LOCAL_GSTACK" && ./setup
|
||||
rm -rf "$LOCAL_GSTACK.bak"
|
||||
```
|
||||
Tell user: "Also updated vendored copy at `$LOCAL_GSTACK` — commit `.claude/skills/gstack/` when you're ready."
|
||||
|
||||
If `./setup` fails, restore from backup and warn the user:
|
||||
```bash
|
||||
rm -rf "$LOCAL_GSTACK"
|
||||
mv "$LOCAL_GSTACK.bak" "$LOCAL_GSTACK"
|
||||
```
|
||||
Tell user: "Sync failed — restored previous version at `$LOCAL_GSTACK`. Run `/gstack-upgrade` manually to retry."
|
||||
|
||||
### Step 4.75: Run version migrations
|
||||
|
||||
After `./setup` completes, run any migration scripts for versions between the old
|
||||
and new version. Migrations handle state fixes that `./setup` alone can't cover
|
||||
(stale config, orphaned files, directory structure changes).
|
||||
|
||||
```bash
|
||||
MIGRATIONS_DIR="$INSTALL_DIR/gstack-upgrade/migrations"
|
||||
if [ -d "$MIGRATIONS_DIR" ]; then
|
||||
for migration in $(find "$MIGRATIONS_DIR" -maxdepth 1 -name 'v*.sh' -type f 2>/dev/null | sort -V); do
|
||||
# Extract version from filename: v0.15.2.0.sh → 0.15.2.0
|
||||
m_ver="$(basename "$migration" .sh | sed 's/^v//')"
|
||||
# Run if this migration version is newer than old version
|
||||
# (simple string compare works for dotted versions with same segment count)
|
||||
if [ "$OLD_VERSION" != "unknown" ] && [ "$(printf '%s\n%s' "$OLD_VERSION" "$m_ver" | sort -V | head -1)" = "$OLD_VERSION" ] && [ "$OLD_VERSION" != "$m_ver" ]; then
|
||||
echo "Running migration $m_ver..."
|
||||
bash "$migration" || echo " Warning: migration $m_ver had errors (non-fatal)"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
```
|
||||
|
||||
Migrations are idempotent bash scripts in `gstack-upgrade/migrations/`. Each is named
|
||||
`v{VERSION}.sh` and runs only when upgrading from an older version. See CONTRIBUTING.md
|
||||
for how to add new migrations.
|
||||
|
||||
### Step 5: Write marker + clear cache
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.gstack
|
||||
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
|
||||
rm -f ~/.gstack/last-update-check
|
||||
rm -f ~/.gstack/update-snoozed
|
||||
```
|
||||
|
||||
### Step 6: Show What's New
|
||||
|
||||
Read `$INSTALL_DIR/CHANGELOG.md`. Find all version entries between the old version and the new version. Summarize as 5-7 bullets grouped by theme. Don't overwhelm — focus on user-facing changes. Skip internal refactors unless they're significant.
|
||||
|
||||
Format:
|
||||
```
|
||||
gstack v{new} — upgraded from v{old}!
|
||||
|
||||
What's new:
|
||||
- [bullet 1]
|
||||
- [bullet 2]
|
||||
- ...
|
||||
|
||||
Happy shipping!
|
||||
```
|
||||
|
||||
### Step 7: Continue
|
||||
|
||||
After showing What's New, continue with whatever skill the user originally invoked. The upgrade is done — no further action needed.
|
||||
|
||||
---
|
||||
|
||||
## Standalone usage
|
||||
|
||||
When invoked directly as `/gstack-upgrade` (not from a preamble):
|
||||
|
||||
1. Force a fresh update check (bypass cache):
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || \
|
||||
.claude/skills/gstack/bin/gstack-update-check --force 2>/dev/null || true
|
||||
```
|
||||
Use the output to determine if an upgrade is available.
|
||||
|
||||
2. If `UPGRADE_AVAILABLE <old> <new>`: follow Steps 2-6 above.
|
||||
|
||||
3. If no output (primary is up to date): check for a stale local vendored copy.
|
||||
|
||||
Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`) and team mode status (`TEAM_MODE`).
|
||||
|
||||
**If `LOCAL_GSTACK` is empty** (no local vendored copy): tell the user "You're already on the latest version (v{version})."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy using the Step 4.5 team-mode removal bash block above. Tell user: "Global v{version} is up to date. Removed stale vendored copy (team mode active). Commit the `.gitignore` change when ready."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`**, compare versions:
|
||||
```bash
|
||||
PRIMARY_VER=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
||||
LOCAL_VER=$(cat "$LOCAL_GSTACK/VERSION" 2>/dev/null || echo "unknown")
|
||||
echo "PRIMARY=$PRIMARY_VER LOCAL=$LOCAL_VER"
|
||||
```
|
||||
|
||||
**If versions differ:** follow the Step 4.5 sync bash block above to update the local copy from the primary. Tell user: "Global v{PRIMARY_VER} is up to date. Updated local vendored copy from v{LOCAL_VER} → v{PRIMARY_VER}. Commit `.claude/skills/gstack/` when you're ready."
|
||||
|
||||
**If versions match:** tell the user "You're on the latest version (v{PRIMARY_VER}). Global and local vendored copy are both up to date."
|
||||
20
gstack-upgrade/migrations/v0.15.2.0.sh
Executable file
20
gstack-upgrade/migrations/v0.15.2.0.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migration: v0.15.2.0 — Fix skill directory structure for unprefixed discovery
|
||||
#
|
||||
# What changed: setup now creates real directories with SKILL.md symlinks
|
||||
# inside instead of directory symlinks. The old pattern (qa -> gstack/qa)
|
||||
# caused Claude Code to auto-prefix skills as "gstack-qa" even with
|
||||
# --no-prefix, because Claude sees the symlink target's parent dir name.
|
||||
#
|
||||
# What this does: runs gstack-relink to recreate all skill entries using
|
||||
# the new real-directory pattern. Idempotent — safe to run multiple times.
|
||||
#
|
||||
# Affected: users who installed gstack before v0.15.2.0 with --no-prefix
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
|
||||
if [ -x "$SCRIPT_DIR/bin/gstack-relink" ]; then
|
||||
echo " [v0.15.2.0] Fixing skill directory structure..."
|
||||
"$SCRIPT_DIR/bin/gstack-relink" 2>/dev/null || true
|
||||
fi
|
||||
54
gstack-upgrade/migrations/v0.16.2.0.sh
Executable file
54
gstack-upgrade/migrations/v0.16.2.0.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migration: v0.16.2.0 — Merge per-project resource logs into builder profile
|
||||
#
|
||||
# What changed: resource dedup moved from per-project resources-shown.jsonl to
|
||||
# the global builder-profile.jsonl (single source of truth for all closing state).
|
||||
#
|
||||
# What this does: finds all per-project resources-shown.jsonl files and merges
|
||||
# their URLs into a stub builder-profile entry so existing users don't lose
|
||||
# their dedup history. Idempotent — safe to run multiple times.
|
||||
#
|
||||
# Affected: users who ran /office-hours before this version
|
||||
set -euo pipefail
|
||||
|
||||
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||
PROFILE_FILE="$GSTACK_HOME/builder-profile.jsonl"
|
||||
|
||||
# Find all per-project resource logs
|
||||
RESOURCE_FILES=$(find "$GSTACK_HOME/projects" -name "resources-shown.jsonl" 2>/dev/null || true)
|
||||
|
||||
if [ -z "$RESOURCE_FILES" ]; then
|
||||
# No per-project resource files exist — clean install, nothing to migrate
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo " [v0.16.2.0] Migrating per-project resource logs to builder profile..."
|
||||
|
||||
# Collect all unique URLs from all per-project files
|
||||
ALL_URLS=$(echo "$RESOURCE_FILES" | while read -r f; do
|
||||
[ -f "$f" ] && cat "$f" 2>/dev/null || true
|
||||
done | grep -o '"url":"[^"]*"' | sed 's/"url":"//;s/"//' | sort -u)
|
||||
|
||||
if [ -z "$ALL_URLS" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if builder-profile already has resource data (idempotency)
|
||||
if [ -f "$PROFILE_FILE" ] && grep -q "resources_shown" "$PROFILE_FILE" 2>/dev/null; then
|
||||
# Already has resource data, check if it includes the migrated URLs
|
||||
EXISTING_URLS=$(grep -o '"resources_shown":\[[^]]*\]' "$PROFILE_FILE" 2>/dev/null | grep -o 'https://[^"]*' | sort -u)
|
||||
NEW_URLS=$(comm -23 <(echo "$ALL_URLS") <(echo "$EXISTING_URLS") 2>/dev/null || echo "$ALL_URLS")
|
||||
if [ -z "$NEW_URLS" ]; then
|
||||
# All URLs already present — nothing to do
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build JSON array of URLs
|
||||
URL_ARRAY=$(echo "$ALL_URLS" | awk 'BEGIN{printf "["} NR>1{printf ","} {printf "\"%s\"", $0} END{printf "]"}')
|
||||
|
||||
# Append a migration stub entry to the builder profile
|
||||
mkdir -p "$GSTACK_HOME"
|
||||
echo "{\"date\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"mode\":\"migration\",\"project_slug\":\"_migrated\",\"signal_count\":0,\"signals\":[],\"design_doc\":\"\",\"assignment\":\"\",\"resources_shown\":$URL_ARRAY,\"topics\":[]}" >> "$PROFILE_FILE"
|
||||
|
||||
echo " [v0.16.2.0] Migrated $(echo "$ALL_URLS" | wc -l | tr -d ' ') resource URLs to builder profile."
|
||||
38
gstack-upgrade/migrations/v1.0.0.0.sh
Executable file
38
gstack-upgrade/migrations/v1.0.0.0.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migration: v1.0.0.0 — V1 writing style prompt
|
||||
#
|
||||
# What changed: tier-≥2 skills default to ELI10 writing style (jargon glossed on
|
||||
# first use, outcome-framed questions, short sentences). Power users who prefer
|
||||
# the older V0 prose can set `gstack-config set explain_level terse`.
|
||||
#
|
||||
# What this does: writes a "pending prompt" flag file. On the first tier-≥2 skill
|
||||
# invocation after upgrade, the preamble reads the flag and asks the user once
|
||||
# whether to keep the new default or opt into terse mode. Flag file is deleted
|
||||
# after the user answers. Idempotent — safe to run multiple times.
|
||||
#
|
||||
# Affected: every user on v0.19.x and below who upgrades to v1.x
|
||||
set -euo pipefail
|
||||
|
||||
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||
PROMPTED_FLAG="$GSTACK_HOME/.writing-style-prompted"
|
||||
PENDING_FLAG="$GSTACK_HOME/.writing-style-prompt-pending"
|
||||
|
||||
mkdir -p "$GSTACK_HOME"
|
||||
|
||||
# If the user has already answered the prompt at any point, skip.
|
||||
if [ -f "$PROMPTED_FLAG" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If the user has already explicitly set explain_level (either way), count that
|
||||
# as an answer — they've made their choice, don't ask again.
|
||||
EXPLAIN_LEVEL_SET="$("${HOME}/.claude/skills/gstack/bin/gstack-config" get explain_level 2>/dev/null || true)"
|
||||
if [ -n "$EXPLAIN_LEVEL_SET" ]; then
|
||||
touch "$PROMPTED_FLAG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Write the pending flag — preamble will see it on the first tier-≥2 skill invocation.
|
||||
touch "$PENDING_FLAG"
|
||||
|
||||
echo " [v1.0.0.0] V1 writing style: you'll see a one-time prompt on your next skill run asking if you want the new default (glossed jargon, outcome framing) or the older terse prose."
|
||||
137
gstack-upgrade/migrations/v1.1.3.0.sh
Executable file
137
gstack-upgrade/migrations/v1.1.3.0.sh
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migration: v1.1.3.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
|
||||
|
||||
# Guard: refuse to run if HOME is unset or empty. With `set -u`, unset HOME
|
||||
# errors out, but HOME="" (possible under sudo-without-H, systemd units, some
|
||||
# CI runners) survives and produces dangerous absolute paths like
|
||||
# "/.claude/skills/...". Abort cleanly.
|
||||
if [ -z "${HOME:-}" ]; then
|
||||
echo " [v1.1.3.0] HOME is unset or empty — skipping migration." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SKILLS_DIR="${HOME}/.claude/skills"
|
||||
OLD_TOPLEVEL="${SKILLS_DIR}/checkpoint"
|
||||
OLD_NAMESPACED="${SKILLS_DIR}/gstack/checkpoint"
|
||||
GSTACK_ROOT_REAL=""
|
||||
|
||||
# Helper: canonical-path a target (symlink-safe). Prints the resolved path, or
|
||||
# empty on failure (broken symlink, ENOENT, ELOOP). Both realpath AND the python3
|
||||
# fallback are tried — a single tool failure shouldn't defeat the ownership
|
||||
# check. Returns empty string if both fail.
|
||||
resolve_real() {
|
||||
local target="$1"
|
||||
local out=""
|
||||
if command -v realpath >/dev/null 2>&1; then
|
||||
out=$(realpath "$target" 2>/dev/null || true)
|
||||
fi
|
||||
if [ -z "$out" ] && command -v python3 >/dev/null 2>&1; then
|
||||
out=$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "$target" 2>/dev/null || true)
|
||||
fi
|
||||
printf '%s' "$out"
|
||||
}
|
||||
|
||||
# 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
|
||||
GSTACK_ROOT_REAL=$(resolve_real "${SKILLS_DIR}/gstack")
|
||||
fi
|
||||
|
||||
# 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.1.3.0] Removed stale /checkpoint symlink (was shadowing Claude Code's /rewind alias)."
|
||||
removed_any=1
|
||||
else
|
||||
echo " [v1.1.3.0] Leaving $OLD_TOPLEVEL alone — symlink target is outside gstack (or unresolvable)."
|
||||
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).
|
||||
# Use find to count real files, ignoring .DS_Store (macOS sidecars).
|
||||
file_count=$(find "$OLD_TOPLEVEL" -maxdepth 1 -type f -not -name '.DS_Store' -not -name '._*' 2>/dev/null | wc -l | tr -d ' ')
|
||||
symlink_count=$(find "$OLD_TOPLEVEL" -maxdepth 1 -type l 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [ "$file_count" = "0" ] && [ "$symlink_count" = "1" ] && [ -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
|
||||
# Strip macOS sidecars first (not user content), then remove the dir.
|
||||
find "$OLD_TOPLEVEL" -maxdepth 1 \( -name '.DS_Store' -o -name '._*' \) -type f -delete 2>/dev/null || true
|
||||
rm -r -- "$OLD_TOPLEVEL"
|
||||
echo " [v1.1.3.0] Removed stale /checkpoint install directory (gstack prefix-mode)."
|
||||
removed_any=1
|
||||
else
|
||||
echo " [v1.1.3.0] Leaving $OLD_TOPLEVEL alone — SKILL.md symlink target is outside gstack."
|
||||
fi
|
||||
else
|
||||
echo " [v1.1.3.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/
|
||||
# Ownership guard applies here too: only remove if this path resolves inside the
|
||||
# gstack skills root. If a user replaced the directory with a symlink pointing
|
||||
# elsewhere (e.g., at their own fork), respect it.
|
||||
if [ -L "$OLD_NAMESPACED" ]; then
|
||||
target_real=$(resolve_real "$OLD_NAMESPACED")
|
||||
if [ -n "$GSTACK_ROOT_REAL" ] && path_inside "$target_real" "$GSTACK_ROOT_REAL"; then
|
||||
rm -- "$OLD_NAMESPACED"
|
||||
echo " [v1.1.3.0] Removed stale ~/.claude/skills/gstack/checkpoint symlink."
|
||||
removed_any=1
|
||||
else
|
||||
echo " [v1.1.3.0] Leaving $OLD_NAMESPACED alone — symlink target is outside gstack."
|
||||
fi
|
||||
elif [ -d "$OLD_NAMESPACED" ]; then
|
||||
# Regular directory. This is the gstack-prefix install location. Check that
|
||||
# it resolves to a path inside the gstack root (it should, unless someone
|
||||
# hand-edited the tree).
|
||||
target_real=$(resolve_real "$OLD_NAMESPACED")
|
||||
if [ -n "$GSTACK_ROOT_REAL" ] && path_inside "$target_real" "$GSTACK_ROOT_REAL"; then
|
||||
rm -rf -- "$OLD_NAMESPACED"
|
||||
echo " [v1.1.3.0] Removed stale ~/.claude/skills/gstack/checkpoint/ (replaced by context-save + context-restore)."
|
||||
removed_any=1
|
||||
else
|
||||
echo " [v1.1.3.0] Leaving $OLD_NAMESPACED alone — resolves outside gstack."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$removed_any" = "1" ]; then
|
||||
echo " [v1.1.3.0] /checkpoint is now Claude Code's native /rewind alias. Use /context-save to save state and /context-restore to resume."
|
||||
fi
|
||||
|
||||
exit 0
|
||||
56
gstack-upgrade/migrations/v1.17.0.0.sh
Executable file
56
gstack-upgrade/migrations/v1.17.0.0.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migration: v1.17.0.0 — Wire existing brain-sync repos as gbrain federated sources
|
||||
#
|
||||
# Pre-1.17.0.0 /setup-gbrain wrote ~/.gstack/consumers.json with a placeholder
|
||||
# `status: "pending"` and an empty `ingest_url`, expecting a gbrain HTTP
|
||||
# /ingest-repo endpoint that never shipped. This migration runs the real
|
||||
# wireup (gbrain sources add + worktree + initial sync) for users who
|
||||
# already opted into brain-sync but never got the gbrain side connected.
|
||||
#
|
||||
# Idempotent: safe to re-run. Skips when:
|
||||
# - User never opted into brain-sync (gbrain_sync_mode = off or unset)
|
||||
# - No ~/.gstack/.git (brain-init never ran)
|
||||
# - The wireup helper is missing on disk (broken install — defensive)
|
||||
#
|
||||
# Failure mode: invokes the helper WITHOUT --strict, so a missing/old gbrain
|
||||
# CLI is a benign skip rather than blocking the rest of /gstack-upgrade.
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${HOME:-}" ]; then
|
||||
echo " [v1.17.0.0] HOME is unset or empty — skipping migration." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SKILLS_DIR="${HOME}/.claude/skills"
|
||||
BIN_DIR="${SKILLS_DIR}/gstack/bin"
|
||||
CONFIG_BIN="${BIN_DIR}/gstack-config"
|
||||
WIREUP_BIN="${BIN_DIR}/gstack-gbrain-source-wireup"
|
||||
|
||||
# Skip if user never opted into brain-sync.
|
||||
SYNC_MODE=""
|
||||
if [ -x "$CONFIG_BIN" ]; then
|
||||
# Trim whitespace defensively: gstack-config can emit trailing newlines,
|
||||
# which would mis-classify "off\n" as a non-empty non-off mode.
|
||||
SYNC_MODE=$("$CONFIG_BIN" get gbrain_sync_mode 2>/dev/null | tr -d '[:space:]' || echo "")
|
||||
fi
|
||||
if [ "$SYNC_MODE" = "off" ] || [ -z "$SYNC_MODE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Skip if no brain-sync git repo exists.
|
||||
if [ ! -d "${HOME}/.gstack/.git" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Skip if helper missing (defensive — should always be present post-upgrade).
|
||||
if [ ! -x "$WIREUP_BIN" ]; then
|
||||
echo " [v1.17.0.0] $WIREUP_BIN missing or non-executable — skipping wireup." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo " [v1.17.0.0] Wiring brain-sync repo into gbrain (federated source + initial sync)..."
|
||||
|
||||
# No --strict: missing/old gbrain is a benign skip during a batch upgrade.
|
||||
"$WIREUP_BIN" || {
|
||||
echo " [v1.17.0.0] Wireup exited non-zero — re-run manually with: $WIREUP_BIN" >&2
|
||||
}
|
||||
344
gstack-upgrade/migrations/v1.27.0.0.sh
Executable file
344
gstack-upgrade/migrations/v1.27.0.0.sh
Executable file
@@ -0,0 +1,344 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migration: v1.27.0.0 — rename gstack-brain-* → gstack-artifacts-*
|
||||
#
|
||||
# Phase C of the v1.27.0.0 plan. Hard-rename, no compat shim. Steps:
|
||||
# 1. gh_repo_renamed — gh/glab repo rename gstack-brain-$USER →
|
||||
# gstack-artifacts-$USER (skipped on user opt-out)
|
||||
# 2. remote_txt_renamed — mv ~/.gstack-brain-remote.txt → artifacts-remote.txt
|
||||
# 3. config_key_renamed — rewrite gbrain_sync_mode → artifacts_sync_mode
|
||||
# in ~/.gstack/config.yaml
|
||||
# 4. claude_md_block_rewritten — find-and-replace any existing GBrain
|
||||
# Configuration block that references "Memory sync"
|
||||
# 5. sources_swapped — gbrain sources add new (verify) → remove old
|
||||
# (codex Finding #6: add-before-remove ordering)
|
||||
# 6. done — write touchfile, delete journal
|
||||
#
|
||||
# Interruption-safe via journal at ~/.gstack/.migrations/v1.27.0.0.journal:
|
||||
# each step writes its name on success; re-entry resumes from the next un-done
|
||||
# step. Done touchfile at ~/.gstack/.migrations/v1.27.0.0.done.
|
||||
#
|
||||
# Three host-mode branches per the plan:
|
||||
# Local CLI + GitHub — all steps run automatically
|
||||
# Local CLI + GitLab — same with glab repo rename
|
||||
# Remote MCP only — steps 1-4 still run; step 5 prints commands for
|
||||
# the brain admin to run on the brain host
|
||||
#
|
||||
# All steps are idempotent. Re-running after partial completion is safe.
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${HOME:-}" ]; then
|
||||
echo " [v1.27.0.0] HOME is unset — skipping migration." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
GSTACK_HOME="${HOME}/.gstack"
|
||||
SKILLS_DIR="${HOME}/.claude/skills"
|
||||
BIN_DIR="${SKILLS_DIR}/gstack/bin"
|
||||
CONFIG_BIN="${BIN_DIR}/gstack-config"
|
||||
URL_BIN="${BIN_DIR}/gstack-artifacts-url"
|
||||
|
||||
MIGRATION_DIR="${GSTACK_HOME}/.migrations"
|
||||
JOURNAL="${MIGRATION_DIR}/v1.27.0.0.journal"
|
||||
DONE="${MIGRATION_DIR}/v1.27.0.0.done"
|
||||
SKIPPED="${MIGRATION_DIR}/v1.27.0.0.skipped-by-user"
|
||||
|
||||
USER_NAME="${USER:-$(whoami 2>/dev/null || echo unknown)}"
|
||||
OLD_REPO_NAME="gstack-brain-${USER_NAME}"
|
||||
NEW_REPO_NAME="gstack-artifacts-${USER_NAME}"
|
||||
OLD_REMOTE_TXT="${HOME}/.gstack-brain-remote.txt"
|
||||
NEW_REMOTE_TXT="${HOME}/.gstack-artifacts-remote.txt"
|
||||
OLD_SOURCE_ID="${OLD_REPO_NAME}"
|
||||
NEW_SOURCE_ID="${NEW_REPO_NAME}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Journal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
mkdir -p "$MIGRATION_DIR"
|
||||
|
||||
# Already done? exit silently.
|
||||
[ -f "$DONE" ] && exit 0
|
||||
|
||||
# User opted out previously? exit silently. (Re-invoke via
|
||||
# `/setup-gbrain --rerun-migration` removes this marker.)
|
||||
[ -f "$SKIPPED" ] && exit 0
|
||||
|
||||
journal_done() {
|
||||
# Returns 0 if the named step is recorded as complete in the journal.
|
||||
local step="$1"
|
||||
[ -f "$JOURNAL" ] && grep -q "^${step}$" "$JOURNAL" 2>/dev/null
|
||||
}
|
||||
|
||||
mark_done() {
|
||||
local step="$1"
|
||||
echo "$step" >> "$JOURNAL"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Detect environment + ask once if there's anything to migrate
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Has the user ever opted into brain sync? Two signals:
|
||||
# - presence of ~/.gstack-brain-remote.txt (legacy file)
|
||||
# - presence of ~/.gstack/.git (brain-init ever ran)
|
||||
HAS_LEGACY_STATE=0
|
||||
[ -f "$OLD_REMOTE_TXT" ] && HAS_LEGACY_STATE=1
|
||||
[ -d "$GSTACK_HOME/.git" ] && HAS_LEGACY_STATE=1
|
||||
|
||||
# If nothing to migrate, finalize silently.
|
||||
if [ "$HAS_LEGACY_STATE" = "0" ]; then
|
||||
echo " [v1.27.0.0] no legacy gstack-brain state detected — nothing to migrate." >&2
|
||||
touch "$DONE"
|
||||
rm -f "$JOURNAL" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Ask once (idempotent: if journal exists from a prior partial run, skip ask).
|
||||
if [ ! -f "$JOURNAL" ]; then
|
||||
cat >&2 <<EOF
|
||||
|
||||
[v1.27.0.0] gstack-brain has been renamed to gstack-artifacts.
|
||||
This is a clearer name for what it actually holds: CEO plans, designs,
|
||||
/investigate reports, retros (i.e. artifacts, not behavioral memory).
|
||||
|
||||
This migration will:
|
||||
1. Rename your private GitHub/GitLab repo "$OLD_REPO_NAME" → "$NEW_REPO_NAME"
|
||||
2. mv ~/.gstack-brain-remote.txt → ~/.gstack-artifacts-remote.txt
|
||||
3. Rename gbrain_sync_mode → artifacts_sync_mode in ~/.gstack/config.yaml
|
||||
4. Update any "## GBrain Configuration" block in CLAUDE.md
|
||||
5. Update gbrain federated source registration (local CLI mode)
|
||||
OR print commands for your brain admin (remote MCP mode)
|
||||
|
||||
Each step is journaled so a Ctrl-C mid-flight is safe to re-run.
|
||||
|
||||
EOF
|
||||
if [ -t 0 ]; then
|
||||
printf " Proceed? [Y/n/skip-for-now]: " >&2
|
||||
read -r REPLY || REPLY=""
|
||||
case "$REPLY" in
|
||||
n|N|no|No|NO)
|
||||
echo " Skipping migration. Re-run via /setup-gbrain --rerun-migration." >&2
|
||||
touch "$SKIPPED"
|
||||
exit 0
|
||||
;;
|
||||
skip|skip-for-now|s)
|
||||
echo " Skipping for now. Will ask again next upgrade." >&2
|
||||
# Don't write SKIPPED — leave both old + new state untouched, ask again next time.
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# Non-interactive (CI, scripted upgrade): proceed automatically.
|
||||
echo " (non-interactive: proceeding automatically)" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Detect host (gh / glab / manual) for steps 1 + 5
|
||||
# ---------------------------------------------------------------------------
|
||||
detect_host() {
|
||||
# Read the canonical-form remote URL (the legacy file in the migration window).
|
||||
local url=""
|
||||
if [ -f "$OLD_REMOTE_TXT" ]; then
|
||||
url=$(head -1 "$OLD_REMOTE_TXT" 2>/dev/null | tr -d '[:space:]' || echo "")
|
||||
elif [ -f "$NEW_REMOTE_TXT" ]; then
|
||||
url=$(head -1 "$NEW_REMOTE_TXT" 2>/dev/null | tr -d '[:space:]' || echo "")
|
||||
fi
|
||||
if echo "$url" | grep -q 'github\.com'; then
|
||||
echo "github"
|
||||
elif echo "$url" | grep -q 'gitlab'; then
|
||||
echo "gitlab"
|
||||
else
|
||||
echo "manual"
|
||||
fi
|
||||
}
|
||||
|
||||
HOST=$(detect_host)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Detect MCP mode (so step 5 knows whether to execute or print)
|
||||
# ---------------------------------------------------------------------------
|
||||
detect_mcp_mode() {
|
||||
# Cheap probe: ~/.claude.json type field. Defense-in-depth tier 3 only;
|
||||
# the migration script avoids invoking `claude` to keep upgrade fast.
|
||||
if command -v jq >/dev/null 2>&1 && [ -f "$HOME/.claude.json" ]; then
|
||||
local t
|
||||
t=$(jq -r '.mcpServers.gbrain.type // .mcpServers.gbrain.transport // empty' "$HOME/.claude.json" 2>/dev/null)
|
||||
case "$t" in
|
||||
url|http|sse) echo "remote-http"; return ;;
|
||||
stdio) echo "local-stdio"; return ;;
|
||||
esac
|
||||
fi
|
||||
echo "none"
|
||||
}
|
||||
|
||||
MCP_MODE=$(detect_mcp_mode)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 1: gh/glab repo rename
|
||||
# ---------------------------------------------------------------------------
|
||||
if ! journal_done "gh_repo_renamed"; then
|
||||
echo " [v1.27.0.0] step 1: rename remote repo $OLD_REPO_NAME → $NEW_REPO_NAME" >&2
|
||||
case "$HOST" in
|
||||
github)
|
||||
if command -v gh >/dev/null 2>&1 && gh auth status >/dev/null 2>&1; then
|
||||
# Idempotent: if new name already exists, treat as success.
|
||||
if gh repo view "$NEW_REPO_NAME" >/dev/null 2>&1; then
|
||||
echo " repo already named $NEW_REPO_NAME on GitHub — no-op" >&2
|
||||
mark_done "gh_repo_renamed"
|
||||
else
|
||||
if gh repo rename "$NEW_REPO_NAME" --repo "$OLD_REPO_NAME" --yes 2>/dev/null \
|
||||
|| gh repo edit "$OLD_REPO_NAME" --name "$NEW_REPO_NAME" 2>/dev/null; then
|
||||
echo " renamed on GitHub" >&2
|
||||
mark_done "gh_repo_renamed"
|
||||
else
|
||||
echo " WARNING: gh rename failed (repo may not exist or permission denied)" >&2
|
||||
echo " skipping step 1; subsequent steps still run" >&2
|
||||
mark_done "gh_repo_renamed"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo " gh CLI not available — skipping rename step (manual: gh repo rename ...)" >&2
|
||||
mark_done "gh_repo_renamed"
|
||||
fi
|
||||
;;
|
||||
gitlab)
|
||||
if command -v glab >/dev/null 2>&1 && glab auth status >/dev/null 2>&1; then
|
||||
if glab repo view "$NEW_REPO_NAME" >/dev/null 2>&1; then
|
||||
echo " repo already named $NEW_REPO_NAME on GitLab — no-op" >&2
|
||||
mark_done "gh_repo_renamed"
|
||||
else
|
||||
# GitLab CLI doesn't have a direct rename; user has to do it via API.
|
||||
echo " glab repo rename isn't a single command on GitLab." >&2
|
||||
echo " Manual: visit your GitLab project Settings → General → Advanced → Rename" >&2
|
||||
echo " or use: glab api projects/:id -X PUT -f name=$NEW_REPO_NAME -f path=$NEW_REPO_NAME" >&2
|
||||
mark_done "gh_repo_renamed"
|
||||
fi
|
||||
else
|
||||
echo " glab not available — manual rename required" >&2
|
||||
mark_done "gh_repo_renamed"
|
||||
fi
|
||||
;;
|
||||
manual|*)
|
||||
echo " unknown host (not github/gitlab) — manual rename required" >&2
|
||||
mark_done "gh_repo_renamed"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 2: rename ~/.gstack-brain-remote.txt → ~/.gstack-artifacts-remote.txt
|
||||
# ---------------------------------------------------------------------------
|
||||
if ! journal_done "remote_txt_renamed"; then
|
||||
echo " [v1.27.0.0] step 2: rename ~/.gstack-brain-remote.txt → ~/.gstack-artifacts-remote.txt" >&2
|
||||
if [ -f "$OLD_REMOTE_TXT" ] && [ ! -f "$NEW_REMOTE_TXT" ]; then
|
||||
# Update the URL inside if the rename happened on the host: replace
|
||||
# gstack-brain-$USER with gstack-artifacts-$USER in the URL.
|
||||
OLD_URL=$(head -1 "$OLD_REMOTE_TXT" 2>/dev/null)
|
||||
NEW_URL=$(echo "$OLD_URL" | sed "s|/${OLD_REPO_NAME}|/${NEW_REPO_NAME}|; s|:${OLD_REPO_NAME}|:${NEW_REPO_NAME}|")
|
||||
echo "$NEW_URL" > "$NEW_REMOTE_TXT"
|
||||
chmod 600 "$NEW_REMOTE_TXT"
|
||||
rm -f "$OLD_REMOTE_TXT"
|
||||
echo " moved + URL rewritten: $OLD_URL → $NEW_URL" >&2
|
||||
elif [ -f "$NEW_REMOTE_TXT" ]; then
|
||||
echo " new file already exists — no-op" >&2
|
||||
rm -f "$OLD_REMOTE_TXT" 2>/dev/null || true
|
||||
else
|
||||
echo " no $OLD_REMOTE_TXT to migrate — no-op" >&2
|
||||
fi
|
||||
mark_done "remote_txt_renamed"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 3: rename gbrain_sync_mode → artifacts_sync_mode in config.yaml
|
||||
# ---------------------------------------------------------------------------
|
||||
if ! journal_done "config_key_renamed"; then
|
||||
echo " [v1.27.0.0] step 3: rename gbrain_sync_mode → artifacts_sync_mode in config.yaml" >&2
|
||||
CFG="$GSTACK_HOME/config.yaml"
|
||||
if [ -f "$CFG" ]; then
|
||||
# Atomic in-place rewrite with a tmpfile.
|
||||
TMP=$(mktemp "${CFG}.v1.27.0.0.XXXXXX")
|
||||
sed -e 's/^gbrain_sync_mode:/artifacts_sync_mode:/' \
|
||||
-e 's/^gbrain_sync_mode_prompted:/artifacts_sync_mode_prompted:/' \
|
||||
"$CFG" > "$TMP" && mv "$TMP" "$CFG"
|
||||
echo " rewritten in place" >&2
|
||||
else
|
||||
echo " no $CFG to migrate — no-op" >&2
|
||||
fi
|
||||
mark_done "config_key_renamed"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 4: rewrite CLAUDE.md "## GBrain Configuration" block fields
|
||||
# ---------------------------------------------------------------------------
|
||||
if ! journal_done "claude_md_block_rewritten"; then
|
||||
echo " [v1.27.0.0] step 4: rewrite CLAUDE.md GBrain Configuration block fields" >&2
|
||||
# Look in cwd's CLAUDE.md (where /setup-gbrain wrote it) and ~/.gstack/CLAUDE.md
|
||||
# if it exists. We can't know every project's CLAUDE.md; users rerunning
|
||||
# /setup-gbrain in any project will overwrite that block fresh anyway.
|
||||
for CMD in "$PWD/CLAUDE.md" "$GSTACK_HOME/CLAUDE.md"; do
|
||||
[ -f "$CMD" ] || continue
|
||||
if grep -q "## GBrain Configuration" "$CMD"; then
|
||||
TMP=$(mktemp "${CMD}.v1.27.0.0.XXXXXX")
|
||||
sed -e 's/^- Memory sync:/- Artifacts sync:/' "$CMD" > "$TMP" && mv "$TMP" "$CMD"
|
||||
echo " rewritten field in $CMD" >&2
|
||||
fi
|
||||
done
|
||||
mark_done "claude_md_block_rewritten"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 5: gbrain sources swap (add-new before remove-old per codex Finding #6)
|
||||
# ---------------------------------------------------------------------------
|
||||
if ! journal_done "sources_swapped"; then
|
||||
echo " [v1.27.0.0] step 5: gbrain federated source rename" >&2
|
||||
if [ "$MCP_MODE" = "remote-http" ]; then
|
||||
# Print commands for the brain admin; we can't execute them locally.
|
||||
cat >&2 <<EOF
|
||||
Remote MCP detected. The local gbrain CLI can't update the brain's
|
||||
federated source registration. Send this to your brain admin:
|
||||
|
||||
gbrain sources add ${NEW_SOURCE_ID} --path <new-clone-path> --federated
|
||||
# verify the new source is searching as expected, then:
|
||||
gbrain sources remove ${OLD_SOURCE_ID} --yes
|
||||
|
||||
(Add-new before remove-old keeps search uninterrupted.)
|
||||
|
||||
EOF
|
||||
mark_done "sources_swapped"
|
||||
elif command -v gbrain >/dev/null 2>&1 && [ -d "$GSTACK_HOME/.git" ]; then
|
||||
# Local CLI mode. Sources point at the worktree path; rename the source
|
||||
# ID add-then-remove. The actual on-disk worktree path stays the same.
|
||||
WORKTREE="${GSTACK_BRAIN_WORKTREE:-$HOME/.gstack-brain-worktree}"
|
||||
if gbrain sources list 2>/dev/null | grep -q "$OLD_SOURCE_ID"; then
|
||||
if gbrain sources add "$NEW_SOURCE_ID" --path "$WORKTREE" --federated 2>/dev/null; then
|
||||
echo " added $NEW_SOURCE_ID" >&2
|
||||
if gbrain sources remove "$OLD_SOURCE_ID" --yes 2>/dev/null; then
|
||||
echo " removed $OLD_SOURCE_ID" >&2
|
||||
else
|
||||
echo " WARNING: failed to remove $OLD_SOURCE_ID; both registered. Run manually:" >&2
|
||||
echo " gbrain sources remove $OLD_SOURCE_ID --yes" >&2
|
||||
fi
|
||||
else
|
||||
echo " WARNING: failed to add $NEW_SOURCE_ID. Old source still registered." >&2
|
||||
fi
|
||||
else
|
||||
echo " no $OLD_SOURCE_ID source registered — no-op" >&2
|
||||
fi
|
||||
mark_done "sources_swapped"
|
||||
else
|
||||
echo " gbrain CLI not available or no ~/.gstack/.git — skipping" >&2
|
||||
mark_done "sources_swapped"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 6: finalize (touchfile + clear journal)
|
||||
# ---------------------------------------------------------------------------
|
||||
touch "$DONE"
|
||||
rm -f "$JOURNAL"
|
||||
|
||||
echo " [v1.27.0.0] migration complete." >&2
|
||||
exit 0
|
||||
92
gstack-upgrade/migrations/v1.37.0.0.sh
Executable file
92
gstack-upgrade/migrations/v1.37.0.0.sh
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migration: v1.37.0.0 — split-engine gbrain (remote MCP brain + optional
|
||||
# local PGLite for code search per worktree).
|
||||
#
|
||||
# Per plan D5: prints a ONE-TIME discoverability notice for existing
|
||||
# Path 4 users who don't yet have a local engine. They learn that
|
||||
# symbol-aware code search (gbrain code-def / code-refs / code-callers)
|
||||
# is now available via /setup-gbrain Step 4.5 if they want it.
|
||||
#
|
||||
# When to print the notice (state match — all conditions must hold):
|
||||
# - ~/.claude.json declares mcpServers.gbrain.{type|transport} = http|sse|url
|
||||
# OR mcpServers.gbrain.url is set (remote-http MCP active)
|
||||
# - ~/.gbrain/config.json is absent (no local engine yet)
|
||||
# - User has not previously opted out via:
|
||||
# ~/.claude/skills/gstack/bin/gstack-config set local_code_index_offered true
|
||||
#
|
||||
# When silent: anything else (Path 1/2/3 users, anyone already on PGLite,
|
||||
# anyone who opted out, anyone without remote-http MCP).
|
||||
#
|
||||
# Idempotency: writes a touchfile at ~/.gstack/.migrations/v1.37.0.0.done
|
||||
# on completion. Re-running this script is silent if the touchfile exists,
|
||||
# OR if local_code_index_offered=true.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${HOME:-}" ]; then
|
||||
echo " [v1.37.0.0] HOME is unset — skipping migration." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
|
||||
MIGRATIONS_DIR="$GSTACK_HOME/.migrations"
|
||||
DONE_TOUCH="$MIGRATIONS_DIR/v1.37.0.0.done"
|
||||
CONFIG_BIN="$HOME/.claude/skills/gstack/bin/gstack-config"
|
||||
CLAUDE_JSON="$HOME/.claude.json"
|
||||
GBRAIN_CONFIG="$HOME/.gbrain/config.json"
|
||||
|
||||
mkdir -p "$MIGRATIONS_DIR"
|
||||
|
||||
# Idempotency: already-ran skips silently.
|
||||
if [ -f "$DONE_TOUCH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# User opt-out skips silently AND records done.
|
||||
if [ -x "$CONFIG_BIN" ]; then
|
||||
if [ "$("$CONFIG_BIN" get local_code_index_offered 2>/dev/null)" = "true" ]; then
|
||||
touch "$DONE_TOUCH"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# State match: remote-http MCP active?
|
||||
is_remote_http_mcp() {
|
||||
[ -f "$CLAUDE_JSON" ] || return 1
|
||||
command -v jq >/dev/null 2>&1 || return 1
|
||||
local mtype murl
|
||||
mtype=$(jq -r '.mcpServers.gbrain.type // .mcpServers.gbrain.transport // empty' "$CLAUDE_JSON" 2>/dev/null)
|
||||
murl=$(jq -r '.mcpServers.gbrain.url // empty' "$CLAUDE_JSON" 2>/dev/null)
|
||||
case "$mtype" in
|
||||
url|http|sse) return 0 ;;
|
||||
esac
|
||||
[ -n "$murl" ] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# State match: local engine absent?
|
||||
is_local_engine_missing() {
|
||||
[ ! -f "$GBRAIN_CONFIG" ]
|
||||
}
|
||||
|
||||
if is_remote_http_mcp && is_local_engine_missing; then
|
||||
cat <<'NOTICE'
|
||||
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ gstack v1.37.0.0 — split-engine gbrain │
|
||||
│ │
|
||||
│ Symbol-aware code search is now available on this machine. │
|
||||
│ Your remote brain at gbrain MCP keeps working as today; you can │
|
||||
│ add a tiny local PGLite (~30s, no accounts) for `gbrain │
|
||||
│ code-def` / `code-refs` / `code-callers` queries per worktree. │
|
||||
│ │
|
||||
│ Run /setup-gbrain to opt in at Step 4.5. Or skip this notice │
|
||||
│ permanently: │
|
||||
│ gstack-config set local_code_index_offered true │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
|
||||
NOTICE
|
||||
fi
|
||||
|
||||
# Always touch done so we don't print again, regardless of state-match outcome.
|
||||
touch "$DONE_TOUCH"
|
||||
102
gstack-upgrade/migrations/v1.38.1.0.sh
Executable file
102
gstack-upgrade/migrations/v1.38.1.0.sh
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migration: v1.38.1.0 — add root-level design + test-plan patterns to
|
||||
# .brain-allowlist, .brain-privacy-map.json, and .gitattributes (#1452).
|
||||
#
|
||||
# Why a migration: gstack-artifacts-init regenerates these files but also
|
||||
# does `git commit + push` on ~/.gstack/, which would clobber user state on
|
||||
# upgrade. Instead, we do targeted per-file in-place repairs.
|
||||
#
|
||||
# Per-file independent — if one file is missing we still repair the others.
|
||||
#
|
||||
# Idempotent: each insertion is gated on `not already present` so re-running
|
||||
# the migration is a no-op.
|
||||
|
||||
# No `set -e` — we intentionally tolerate per-file failures so other repairs
|
||||
# still run. `set -u` is fine.
|
||||
set -u
|
||||
|
||||
GSTACK_HOME="${HOME}/.gstack"
|
||||
ALLOWLIST="${GSTACK_HOME}/.brain-allowlist"
|
||||
PRIVACY="${GSTACK_HOME}/.brain-privacy-map.json"
|
||||
GITATTRS="${GSTACK_HOME}/.gitattributes"
|
||||
|
||||
MIGRATION_DIR="${GSTACK_HOME}/.migrations"
|
||||
DONE="${MIGRATION_DIR}/v1.38.1.0.done"
|
||||
|
||||
mkdir -p "${MIGRATION_DIR}" 2>/dev/null || true
|
||||
if [ -f "${DONE}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
NEW_PATTERNS=(
|
||||
'projects/*/*-design-*.md'
|
||||
'projects/*/*-test-plan-*.md'
|
||||
)
|
||||
|
||||
added_any=0
|
||||
|
||||
# ----- .brain-allowlist ---------------------------------------------------
|
||||
if [ -f "${ALLOWLIST}" ]; then
|
||||
for PATTERN in "${NEW_PATTERNS[@]}"; do
|
||||
if ! grep -Fq -- "${PATTERN}" "${ALLOWLIST}" 2>/dev/null; then
|
||||
# Insert before USER ADDITIONS marker. BSD sed (-i.bak) compat for macOS;
|
||||
# the backup file is removed afterward.
|
||||
if grep -q '^# ---- USER ADDITIONS BELOW' "${ALLOWLIST}" 2>/dev/null; then
|
||||
sed -i.bak "/^# ---- USER ADDITIONS BELOW/i\\
|
||||
${PATTERN}
|
||||
" "${ALLOWLIST}" && rm -f "${ALLOWLIST}.bak"
|
||||
added_any=1
|
||||
else
|
||||
# Marker missing — append at end of file as a fallback. User may have
|
||||
# custom-edited the file; better to add than skip silently.
|
||||
printf '%s\n' "${PATTERN}" >> "${ALLOWLIST}"
|
||||
added_any=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ----- .brain-privacy-map.json -------------------------------------------
|
||||
# Uses jq to preserve JSON validity. Skips with a warning if jq is missing.
|
||||
if [ -f "${PRIVACY}" ]; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
for PATTERN in "${NEW_PATTERNS[@]}"; do
|
||||
if ! jq -e --arg p "${PATTERN}" 'map(select(.pattern == $p)) | length > 0' "${PRIVACY}" >/dev/null 2>&1; then
|
||||
if jq --arg p "${PATTERN}" '. += [{"pattern": $p, "class": "artifact"}]' "${PRIVACY}" > "${PRIVACY}.tmp" 2>/dev/null; then
|
||||
mv "${PRIVACY}.tmp" "${PRIVACY}"
|
||||
added_any=1
|
||||
else
|
||||
rm -f "${PRIVACY}.tmp"
|
||||
echo " [v1.38.1.0] WARN: jq failed to patch ${PRIVACY}; skipping pattern ${PATTERN}." >&2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo " [v1.38.1.0] WARN: jq not found; skipping privacy-map repair. Install jq and re-run gstack-upgrade, or run gstack-artifacts-init manually." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ----- .gitattributes -----------------------------------------------------
|
||||
if [ -f "${GITATTRS}" ]; then
|
||||
for PATTERN in "${NEW_PATTERNS[@]}"; do
|
||||
RULE="${PATTERN} merge=union"
|
||||
if ! grep -Fq -- "${RULE}" "${GITATTRS}" 2>/dev/null; then
|
||||
printf '%s\n' "${RULE}" >> "${GITATTRS}"
|
||||
added_any=1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Mark done. Even if no patches were applied (already-current install), we
|
||||
# write the touchfile so the migration runs once.
|
||||
touch "${DONE}"
|
||||
|
||||
if [ "${added_any}" = "1" ]; then
|
||||
echo " [v1.38.1.0] allowlist/privacy-map/gitattributes patched for root-level design + test-plan artifacts (idempotent)" >&2
|
||||
fi
|
||||
|
||||
# NEVER `git commit + push` from this migration. The user controls when the
|
||||
# patches ship into their federated artifacts repo (next gstack-brain-sync
|
||||
# --once or a manual commit).
|
||||
|
||||
exit 0
|
||||
97
gstack-upgrade/migrations/v1.40.0.0.sh
Executable file
97
gstack-upgrade/migrations/v1.40.0.0.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env bash
|
||||
# Migration: v1.40.0.0 — add eng-review-test-plan project-root pattern to
|
||||
# .brain-allowlist, .brain-privacy-map.json, and .gitattributes (#1452 follow-on).
|
||||
#
|
||||
# Why a second migration: v1.38.1.0 shipped two of three filenames for #1452
|
||||
# (`*-design-*.md` and `*-test-plan-*.md`) but missed `/plan-eng-review`'s
|
||||
# actual filename: `*-eng-review-test-plan-*.md`. The v1.38.1.0 migration has
|
||||
# a done-marker, so a "fix v1.38.1.0 and re-run" approach silently no-ops on
|
||||
# existing users. v1.40.0.0 needs its own migration to patch installs that
|
||||
# already ran v1.38.1.0.
|
||||
#
|
||||
# Per-file independent — if one file is missing we still repair the others.
|
||||
#
|
||||
# Idempotent: each insertion is gated on `not already present` so re-running
|
||||
# the migration is a no-op.
|
||||
|
||||
set -u
|
||||
|
||||
GSTACK_HOME="${HOME}/.gstack"
|
||||
ALLOWLIST="${GSTACK_HOME}/.brain-allowlist"
|
||||
PRIVACY="${GSTACK_HOME}/.brain-privacy-map.json"
|
||||
GITATTRS="${GSTACK_HOME}/.gitattributes"
|
||||
|
||||
MIGRATION_DIR="${GSTACK_HOME}/.migrations"
|
||||
DONE="${MIGRATION_DIR}/v1.40.0.0.done"
|
||||
|
||||
mkdir -p "${MIGRATION_DIR}" 2>/dev/null || true
|
||||
if [ -f "${DONE}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
NEW_PATTERNS=(
|
||||
'projects/*/*-eng-review-test-plan-*.md'
|
||||
)
|
||||
|
||||
added_any=0
|
||||
|
||||
# ----- .brain-allowlist ---------------------------------------------------
|
||||
if [ -f "${ALLOWLIST}" ]; then
|
||||
for PATTERN in "${NEW_PATTERNS[@]}"; do
|
||||
if ! grep -Fq -- "${PATTERN}" "${ALLOWLIST}" 2>/dev/null; then
|
||||
if grep -q '^# ---- USER ADDITIONS BELOW' "${ALLOWLIST}" 2>/dev/null; then
|
||||
sed -i.bak "/^# ---- USER ADDITIONS BELOW/i\\
|
||||
${PATTERN}
|
||||
" "${ALLOWLIST}" && rm -f "${ALLOWLIST}.bak"
|
||||
added_any=1
|
||||
else
|
||||
printf '%s\n' "${PATTERN}" >> "${ALLOWLIST}"
|
||||
added_any=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# ----- .brain-privacy-map.json -------------------------------------------
|
||||
if [ -f "${PRIVACY}" ]; then
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
for PATTERN in "${NEW_PATTERNS[@]}"; do
|
||||
if ! jq -e --arg p "${PATTERN}" 'map(select(.pattern == $p)) | length > 0' "${PRIVACY}" >/dev/null 2>&1; then
|
||||
if jq --arg p "${PATTERN}" '. += [{"pattern": $p, "class": "artifact"}]' "${PRIVACY}" > "${PRIVACY}.tmp" 2>/dev/null; then
|
||||
mv "${PRIVACY}.tmp" "${PRIVACY}"
|
||||
added_any=1
|
||||
else
|
||||
rm -f "${PRIVACY}.tmp"
|
||||
echo " [v1.40.0.0] WARN: jq failed to patch ${PRIVACY}; skipping pattern ${PATTERN}." >&2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo " [v1.40.0.0] WARN: jq not found; skipping privacy-map repair. Install jq and re-run gstack-upgrade, or run gstack-artifacts-init manually." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ----- .gitattributes -----------------------------------------------------
|
||||
if [ -f "${GITATTRS}" ]; then
|
||||
for PATTERN in "${NEW_PATTERNS[@]}"; do
|
||||
RULE="${PATTERN} merge=union"
|
||||
if ! grep -Fq -- "${RULE}" "${GITATTRS}" 2>/dev/null; then
|
||||
printf '%s\n' "${RULE}" >> "${GITATTRS}"
|
||||
added_any=1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Mark done even if no patches needed — a fresh-init user's
|
||||
# bin/gstack-artifacts-init now writes the pattern directly, so re-runs
|
||||
# should no-op. The touchfile keeps the migration runner from looping.
|
||||
touch "${DONE}"
|
||||
|
||||
if [ "${added_any}" = "1" ]; then
|
||||
echo " [v1.40.0.0] allowlist/privacy-map/gitattributes patched for /plan-eng-review test plans (idempotent)" >&2
|
||||
fi
|
||||
|
||||
# NEVER `git commit + push` from this migration. The user controls when the
|
||||
# patches ship into their federated artifacts repo.
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user