mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-21 03:40:00 +08:00
feat: smart update check with auto-upgrade, snooze backoff, config CLI (v0.3.9) (#62)
* feat: add bin/gstack-config CLI for reading/writing ~/.gstack/config.yaml Simple get/set/list interface for persistent gstack configuration. Used by update-check and upgrade skill for auto_upgrade and update_check settings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: smart update check with 12h cache, snooze backoff, config disable - Reduce cache TTL from 24h to 12h for faster update detection - Add exponential snooze backoff: 24h → 48h → 1 week (resets on new version) - Add update_check: false config option to disable checks entirely - Clear snooze file on just-upgraded - 14 new tests covering snooze levels, expiry, corruption, and config paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: upgrade skill with auto-upgrade, 4-option prompt, vendored sync - Auto-upgrade mode via config or GSTACK_AUTO_UPGRADE=1 env var - 4-option AskUserQuestion: upgrade once, always, not now, never - Step 4.5: sync local vendored copy after upgrading primary install - Snooze write with escalating backoff on "Not now" - Update preamble text in gen-skill-docs for new upgrade flow - Regenerate all SKILL.md files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: simplify upgrade instructions, move auto-upgrade to completed README now points to /gstack-upgrade instead of long paste commands. Auto-upgrade TODO moved to Completed section (v0.3.8). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: bump version and changelog (v0.3.9) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.3.9 — 2026-03-15
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **`bin/gstack-config` CLI** — simple get/set/list interface for `~/.gstack/config.yaml`. Used by update-check and upgrade skill for persistent settings (auto_upgrade, update_check).
|
||||||
|
- **Smart update check** — 12h cache TTL (was 24h), exponential snooze backoff (24h → 48h → 1 week) when user declines upgrades, `update_check: false` config option to disable checks entirely. Snooze resets when a new version is released.
|
||||||
|
- **Auto-upgrade mode** — set `auto_upgrade: true` in config or `GSTACK_AUTO_UPGRADE=1` env var to skip the upgrade prompt and update automatically.
|
||||||
|
- **4-option upgrade prompt** — "Yes, upgrade now", "Always keep me up to date", "Not now" (snooze), "Never ask again" (disable).
|
||||||
|
- **Vendored copy sync** — `/gstack-upgrade` now detects and updates local vendored copies in the current project after upgrading the primary install.
|
||||||
|
- 25 new tests: 11 for gstack-config CLI, 14 for snooze/config paths in update-check.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- README upgrade/troubleshooting sections simplified to reference `/gstack-upgrade` instead of long paste commands.
|
||||||
|
- Upgrade skill template bumped to v1.1.0 with `Write` tool permission for config editing.
|
||||||
|
- All SKILL.md preambles updated with new upgrade flow description.
|
||||||
|
|
||||||
## 0.3.8 — 2026-03-14
|
## 0.3.8 — 2026-03-14
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -598,18 +598,16 @@ Run `cd ~/.claude/skills/gstack && ./setup` (or `cd .claude/skills/gstack && ./s
|
|||||||
Run `cd ~/.claude/skills/gstack && bun install && bun run build`. This compiles the browser binary. Requires Bun v1.0+.
|
Run `cd ~/.claude/skills/gstack && bun install && bun run build`. This compiles the browser binary. Requires Bun v1.0+.
|
||||||
|
|
||||||
**Project copy is stale?**
|
**Project copy is stale?**
|
||||||
Re-copy from global: `for s in browse plan-ceo-review plan-eng-review review ship retro qa setup-browser-cookies; do rm -f .claude/skills/$s; done && rm -rf .claude/skills/gstack && cp -Rf ~/.claude/skills/gstack .claude/skills/gstack && rm -rf .claude/skills/gstack/.git && cd .claude/skills/gstack && ./setup`
|
Run `/gstack-upgrade` — it updates both the global install and any vendored project copy automatically.
|
||||||
|
|
||||||
**`bun` not installed?**
|
**`bun` not installed?**
|
||||||
Install it: `curl -fsSL https://bun.sh/install | bash`
|
Install it: `curl -fsSL https://bun.sh/install | bash`
|
||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
Paste this into Claude Code:
|
Run `/gstack-upgrade` in Claude Code. It detects your install type (global or vendored), upgrades, syncs any project copies, and shows what's new.
|
||||||
|
|
||||||
> Update gstack: run `cd ~/.claude/skills/gstack && git fetch origin && git reset --hard origin/main && ./setup`. If this project also has gstack at .claude/skills/gstack, update it too: run `for s in browse plan-ceo-review plan-eng-review review ship retro qa setup-browser-cookies; do rm -f .claude/skills/$s; done && rm -rf .claude/skills/gstack && cp -Rf ~/.claude/skills/gstack .claude/skills/gstack && rm -rf .claude/skills/gstack/.git && cd .claude/skills/gstack && ./setup`
|
Or set `auto_upgrade: true` in `~/.gstack/config.yaml` to upgrade automatically whenever a new version is available.
|
||||||
|
|
||||||
The `setup` script rebuilds the browser binary and re-symlinks skills. It takes a few seconds.
|
|
||||||
|
|
||||||
## Uninstalling
|
## Uninstalling
|
||||||
|
|
||||||
|
|||||||
2
SKILL.md
2
SKILL.md
@@ -23,7 +23,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
|
|||||||
[ -n "$_UPD" ] && echo "$_UPD" || true
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||||
|
|
||||||
# gstack browse: QA Testing & Dogfooding
|
# gstack browse: QA Testing & Dogfooding
|
||||||
|
|
||||||
|
|||||||
15
TODOS.md
15
TODOS.md
@@ -338,17 +338,6 @@
|
|||||||
**Effort:** XS
|
**Effort:** XS
|
||||||
**Priority:** P2
|
**Priority:** P2
|
||||||
|
|
||||||
### Auto-upgrade mode (zero-prompt)
|
|
||||||
|
|
||||||
**What:** `GSTACK_AUTO_UPGRADE=1` env var or `~/.gstack/config` option that skips the AskUserQuestion prompt and upgrades automatically.
|
|
||||||
|
|
||||||
**Why:** Power users and CI environments want zero-friction upgrades.
|
|
||||||
|
|
||||||
**Context:** Current upgrade system (v0.3.4) always prompts. This adds opt-in bypass. ~10 lines in preamble instructions.
|
|
||||||
|
|
||||||
**Effort:** S
|
|
||||||
**Priority:** P3
|
|
||||||
|
|
||||||
### Eval web dashboard
|
### Eval web dashboard
|
||||||
|
|
||||||
**What:** `bun run eval:dashboard` serves local HTML with charts: cost trending, detection rate, pass/fail history.
|
**What:** `bun run eval:dashboard` serves local HTML with charts: cost trending, detection rate, pass/fail history.
|
||||||
@@ -395,3 +384,7 @@
|
|||||||
### E2E test cost tracking
|
### E2E test cost tracking
|
||||||
- Track cumulative API spend, warn if over threshold
|
- Track cumulative API spend, warn if over threshold
|
||||||
**Completed:** v0.3.6
|
**Completed:** v0.3.6
|
||||||
|
|
||||||
|
### Auto-upgrade mode + smart update check
|
||||||
|
- Config CLI (`bin/gstack-config`), auto-upgrade via `~/.gstack/config.yaml`, 12h cache TTL, exponential snooze backoff (24h→48h→1wk), "never ask again" option, vendored copy sync on upgrade
|
||||||
|
**Completed:** v0.3.8
|
||||||
|
|||||||
38
bin/gstack-config
Executable file
38
bin/gstack-config
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# gstack-config — read/write ~/.gstack/config.yaml
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# gstack-config get <key> — read a config value
|
||||||
|
# gstack-config set <key> <value> — write a config value
|
||||||
|
# gstack-config list — show all config
|
||||||
|
#
|
||||||
|
# Env overrides (for testing):
|
||||||
|
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
||||||
|
CONFIG_FILE="$STATE_DIR/config.yaml"
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
get)
|
||||||
|
KEY="${2:?Usage: gstack-config get <key>}"
|
||||||
|
grep -E "^${KEY}:" "$CONFIG_FILE" 2>/dev/null | tail -1 | awk '{print $2}' | tr -d '[:space:]' || true
|
||||||
|
;;
|
||||||
|
set)
|
||||||
|
KEY="${2:?Usage: gstack-config set <key> <value>}"
|
||||||
|
VALUE="${3:?Usage: gstack-config set <key> <value>}"
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
if grep -qE "^${KEY}:" "$CONFIG_FILE" 2>/dev/null; then
|
||||||
|
sed -i '' "s/^${KEY}:.*/${KEY}: ${VALUE}/" "$CONFIG_FILE"
|
||||||
|
else
|
||||||
|
echo "${KEY}: ${VALUE}" >> "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
list)
|
||||||
|
cat "$CONFIG_FILE" 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: gstack-config {get|set|list} [key] [value]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# gstack-update-check — daily version check for all skills.
|
# gstack-update-check — periodic version check for all skills.
|
||||||
#
|
#
|
||||||
# Output (one line, or nothing):
|
# Output (one line, or nothing):
|
||||||
# JUST_UPGRADED <old> <new> — marker found from recent upgrade
|
# JUST_UPGRADED <old> <new> — marker found from recent upgrade
|
||||||
# UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
|
# UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
|
||||||
# (nothing) — up to date or check skipped
|
# (nothing) — up to date, snoozed, disabled, or check skipped
|
||||||
#
|
#
|
||||||
# Env overrides (for testing):
|
# Env overrides (for testing):
|
||||||
# GSTACK_DIR — override auto-detected gstack root
|
# GSTACK_DIR — override auto-detected gstack root
|
||||||
@@ -16,9 +16,65 @@ GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|||||||
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
||||||
CACHE_FILE="$STATE_DIR/last-update-check"
|
CACHE_FILE="$STATE_DIR/last-update-check"
|
||||||
MARKER_FILE="$STATE_DIR/just-upgraded-from"
|
MARKER_FILE="$STATE_DIR/just-upgraded-from"
|
||||||
|
SNOOZE_FILE="$STATE_DIR/update-snoozed"
|
||||||
VERSION_FILE="$GSTACK_DIR/VERSION"
|
VERSION_FILE="$GSTACK_DIR/VERSION"
|
||||||
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
|
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
|
||||||
|
|
||||||
|
# ─── Step 0: Check if updates are disabled ────────────────────
|
||||||
|
_UC=$("$GSTACK_DIR/bin/gstack-config" get update_check 2>/dev/null || true)
|
||||||
|
if [ "$_UC" = "false" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ─── Snooze helper ──────────────────────────────────────────
|
||||||
|
# check_snooze <remote_version>
|
||||||
|
# Returns 0 if snoozed (should stay quiet), 1 if not snoozed (should output).
|
||||||
|
#
|
||||||
|
# Snooze file format: <version> <level> <epoch>
|
||||||
|
# Level durations: 1=24h, 2=48h, 3+=7d
|
||||||
|
# New version (version mismatch) resets snooze.
|
||||||
|
check_snooze() {
|
||||||
|
local remote_ver="$1"
|
||||||
|
if [ ! -f "$SNOOZE_FILE" ]; then
|
||||||
|
return 1 # no snooze file → not snoozed
|
||||||
|
fi
|
||||||
|
local snoozed_ver snoozed_level snoozed_epoch
|
||||||
|
snoozed_ver="$(awk '{print $1}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
||||||
|
snoozed_level="$(awk '{print $2}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
||||||
|
snoozed_epoch="$(awk '{print $3}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
# Validate: all three fields must be non-empty
|
||||||
|
if [ -z "$snoozed_ver" ] || [ -z "$snoozed_level" ] || [ -z "$snoozed_epoch" ]; then
|
||||||
|
return 1 # corrupt file → not snoozed
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate: level and epoch must be integers
|
||||||
|
case "$snoozed_level" in *[!0-9]*) return 1 ;; esac
|
||||||
|
case "$snoozed_epoch" in *[!0-9]*) return 1 ;; esac
|
||||||
|
|
||||||
|
# New version dropped? Ignore snooze.
|
||||||
|
if [ "$snoozed_ver" != "$remote_ver" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compute snooze duration based on level
|
||||||
|
local duration
|
||||||
|
case "$snoozed_level" in
|
||||||
|
1) duration=86400 ;; # 24 hours
|
||||||
|
2) duration=172800 ;; # 48 hours
|
||||||
|
*) duration=604800 ;; # 7 days (level 3+)
|
||||||
|
esac
|
||||||
|
|
||||||
|
local now
|
||||||
|
now="$(date +%s)"
|
||||||
|
local expires=$(( snoozed_epoch + duration ))
|
||||||
|
if [ "$now" -lt "$expires" ]; then
|
||||||
|
return 0 # still snoozed
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1 # snooze expired
|
||||||
|
}
|
||||||
|
|
||||||
# ─── Step 1: Read local version ──────────────────────────────
|
# ─── Step 1: Read local version ──────────────────────────────
|
||||||
LOCAL=""
|
LOCAL=""
|
||||||
if [ -f "$VERSION_FILE" ]; then
|
if [ -f "$VERSION_FILE" ]; then
|
||||||
@@ -32,6 +88,7 @@ fi
|
|||||||
if [ -f "$MARKER_FILE" ]; then
|
if [ -f "$MARKER_FILE" ]; then
|
||||||
OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')"
|
OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')"
|
||||||
rm -f "$MARKER_FILE"
|
rm -f "$MARKER_FILE"
|
||||||
|
rm -f "$SNOOZE_FILE"
|
||||||
mkdir -p "$STATE_DIR"
|
mkdir -p "$STATE_DIR"
|
||||||
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
||||||
if [ -n "$OLD" ]; then
|
if [ -n "$OLD" ]; then
|
||||||
@@ -40,10 +97,10 @@ if [ -f "$MARKER_FILE" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ─── Step 3: Check cache freshness (24h = 1440 min) ──────────
|
# ─── Step 3: Check cache freshness (12h = 720 min) ──────────
|
||||||
if [ -f "$CACHE_FILE" ]; then
|
if [ -f "$CACHE_FILE" ]; then
|
||||||
# Cache is fresh if modified within 1440 minutes
|
# Cache is fresh if modified within 720 minutes
|
||||||
STALE=$(find "$CACHE_FILE" -mmin +1440 2>/dev/null || true)
|
STALE=$(find "$CACHE_FILE" -mmin +720 2>/dev/null || true)
|
||||||
if [ -z "$STALE" ]; then
|
if [ -z "$STALE" ]; then
|
||||||
# Cache is fresh — read it
|
# Cache is fresh — read it
|
||||||
CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)"
|
CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)"
|
||||||
@@ -60,6 +117,10 @@ if [ -f "$CACHE_FILE" ]; then
|
|||||||
# Verify local version still matches cached old version
|
# Verify local version still matches cached old version
|
||||||
CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')"
|
CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')"
|
||||||
if [ "$CACHED_OLD" = "$LOCAL" ]; then
|
if [ "$CACHED_OLD" = "$LOCAL" ]; then
|
||||||
|
CACHED_NEW="$(echo "$CACHED" | awk '{print $3}')"
|
||||||
|
if check_snooze "$CACHED_NEW"; then
|
||||||
|
exit 0 # snoozed — stay quiet
|
||||||
|
fi
|
||||||
echo "$CACHED"
|
echo "$CACHED"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -90,4 +151,7 @@ fi
|
|||||||
|
|
||||||
# Versions differ — upgrade available
|
# Versions differ — upgrade available
|
||||||
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
|
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
|
||||||
|
if check_snooze "$REMOTE"; then
|
||||||
|
exit 0 # snoozed — stay quiet
|
||||||
|
fi
|
||||||
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"
|
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
|
|||||||
[ -n "$_UPD" ] && echo "$_UPD" || true
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||||
|
|
||||||
# browse: QA Testing & Dogfooding
|
# browse: QA Testing & Dogfooding
|
||||||
|
|
||||||
|
|||||||
125
browse/test/gstack-config.test.ts
Normal file
125
browse/test/gstack-config.test.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* Tests for bin/gstack-config bash script.
|
||||||
|
*
|
||||||
|
* Uses Bun.spawnSync to invoke the script with temp dirs and
|
||||||
|
* GSTACK_STATE_DIR env override for full isolation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||||
|
import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
|
||||||
|
const SCRIPT = join(import.meta.dir, '..', '..', 'bin', 'gstack-config');
|
||||||
|
|
||||||
|
let stateDir: string;
|
||||||
|
|
||||||
|
function run(args: string[] = [], extraEnv: Record<string, string> = {}) {
|
||||||
|
const result = Bun.spawnSync(['bash', SCRIPT, ...args], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
GSTACK_STATE_DIR: stateDir,
|
||||||
|
...extraEnv,
|
||||||
|
},
|
||||||
|
stdout: 'pipe',
|
||||||
|
stderr: 'pipe',
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
exitCode: result.exitCode,
|
||||||
|
stdout: result.stdout.toString().trim(),
|
||||||
|
stderr: result.stderr.toString().trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stateDir = mkdtempSync(join(tmpdir(), 'gstack-config-test-'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(stateDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('gstack-config', () => {
|
||||||
|
// ─── get ──────────────────────────────────────────────────
|
||||||
|
test('get on missing file returns empty, exit 0', () => {
|
||||||
|
const { exitCode, stdout } = run(['get', 'auto_upgrade']);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get existing key returns value', () => {
|
||||||
|
writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: true\n');
|
||||||
|
const { exitCode, stdout } = run(['get', 'auto_upgrade']);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get missing key returns empty', () => {
|
||||||
|
writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: true\n');
|
||||||
|
const { exitCode, stdout } = run(['get', 'nonexistent']);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get returns last value when key appears multiple times', () => {
|
||||||
|
writeFileSync(join(stateDir, 'config.yaml'), 'foo: bar\nfoo: baz\n');
|
||||||
|
const { exitCode, stdout } = run(['get', 'foo']);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('baz');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── set ──────────────────────────────────────────────────
|
||||||
|
test('set creates file and writes key on missing file', () => {
|
||||||
|
const { exitCode } = run(['set', 'auto_upgrade', 'true']);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
const content = readFileSync(join(stateDir, 'config.yaml'), 'utf-8');
|
||||||
|
expect(content).toContain('auto_upgrade: true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set appends new key to existing file', () => {
|
||||||
|
writeFileSync(join(stateDir, 'config.yaml'), 'foo: bar\n');
|
||||||
|
const { exitCode } = run(['set', 'auto_upgrade', 'true']);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
const content = readFileSync(join(stateDir, 'config.yaml'), 'utf-8');
|
||||||
|
expect(content).toContain('foo: bar');
|
||||||
|
expect(content).toContain('auto_upgrade: true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set replaces existing key in-place', () => {
|
||||||
|
writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: false\n');
|
||||||
|
const { exitCode } = run(['set', 'auto_upgrade', 'true']);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
const content = readFileSync(join(stateDir, 'config.yaml'), 'utf-8');
|
||||||
|
expect(content).toContain('auto_upgrade: true');
|
||||||
|
expect(content).not.toContain('auto_upgrade: false');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set creates state dir if missing', () => {
|
||||||
|
const nestedDir = join(stateDir, 'nested', 'dir');
|
||||||
|
const { exitCode } = run(['set', 'foo', 'bar'], { GSTACK_STATE_DIR: nestedDir });
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(existsSync(join(nestedDir, 'config.yaml'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── list ─────────────────────────────────────────────────
|
||||||
|
test('list shows all keys', () => {
|
||||||
|
writeFileSync(join(stateDir, 'config.yaml'), 'auto_upgrade: true\nupdate_check: false\n');
|
||||||
|
const { exitCode, stdout } = run(['list']);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toContain('auto_upgrade: true');
|
||||||
|
expect(stdout).toContain('update_check: false');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('list on missing file returns empty, exit 0', () => {
|
||||||
|
const { exitCode, stdout } = run(['list']);
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── usage ────────────────────────────────────────────────
|
||||||
|
test('no args shows usage and exits 1', () => {
|
||||||
|
const { exitCode, stdout } = run([]);
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(stdout).toContain('Usage');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||||
import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync } from 'fs';
|
import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync, mkdirSync, symlinkSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
|
|
||||||
@@ -38,6 +38,10 @@ function run(extraEnv: Record<string, string> = {}) {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
gstackDir = mkdtempSync(join(tmpdir(), 'gstack-upd-test-'));
|
gstackDir = mkdtempSync(join(tmpdir(), 'gstack-upd-test-'));
|
||||||
stateDir = mkdtempSync(join(tmpdir(), 'gstack-state-test-'));
|
stateDir = mkdtempSync(join(tmpdir(), 'gstack-state-test-'));
|
||||||
|
// Link real gstack-config so update_check config check works
|
||||||
|
const binDir = join(gstackDir, 'bin');
|
||||||
|
mkdirSync(binDir);
|
||||||
|
symlinkSync(join(import.meta.dir, '..', '..', 'bin', 'gstack-config'), join(binDir, 'gstack-config'));
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -45,6 +49,18 @@ afterEach(() => {
|
|||||||
rmSync(stateDir, { recursive: true, force: true });
|
rmSync(stateDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function writeSnooze(version: string, level: number, epochSeconds: number) {
|
||||||
|
writeFileSync(join(stateDir, 'update-snoozed'), `${version} ${level} ${epochSeconds}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeConfig(content: string) {
|
||||||
|
writeFileSync(join(stateDir, 'config.yaml'), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nowEpoch(): number {
|
||||||
|
return Math.floor(Date.now() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
describe('gstack-update-check', () => {
|
describe('gstack-update-check', () => {
|
||||||
// ─── Path A: No VERSION file ────────────────────────────────
|
// ─── Path A: No VERSION file ────────────────────────────────
|
||||||
test('exits 0 with no output when VERSION file is missing', () => {
|
test('exits 0 with no output when VERSION file is missing', () => {
|
||||||
@@ -246,4 +262,154 @@ describe('gstack-update-check', () => {
|
|||||||
expect(third.exitCode).toBe(0);
|
expect(third.exitCode).toBe(0);
|
||||||
expect(third.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
expect(third.stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ─── Snooze tests ───────────────────────────────────────────
|
||||||
|
test('snoozed level 1 within 24h → silent (cached path)', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
writeSnooze('0.4.0', 1, nowEpoch() - 3600); // 1h ago (within 24h)
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('snoozed level 1 expired (25h ago) → outputs UPGRADE_AVAILABLE', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
writeSnooze('0.4.0', 1, nowEpoch() - 90000); // 25h ago
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('snoozed level 2 within 48h → silent', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
writeSnooze('0.4.0', 2, nowEpoch() - 86400); // 24h ago (within 48h)
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('snoozed level 2 expired (49h ago) → outputs', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
writeSnooze('0.4.0', 2, nowEpoch() - 176400); // 49h ago
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('snoozed level 3 within 7d → silent', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
writeSnooze('0.4.0', 3, nowEpoch() - 518400); // 6d ago (within 7d)
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('snoozed level 3 expired (8d ago) → outputs', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
writeSnooze('0.4.0', 3, nowEpoch() - 691200); // 8d ago
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('snooze ignored when version differs (new version resets snooze)', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.5.0');
|
||||||
|
// Snoozed for 0.4.0, but remote is now 0.5.0
|
||||||
|
writeSnooze('0.4.0', 3, nowEpoch() - 60); // very recent
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.5.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('corrupt snooze file → outputs normally', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
writeFileSync(join(stateDir, 'update-snoozed'), 'garbage');
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-numeric epoch in snooze file → outputs', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
writeFileSync(join(stateDir, 'update-snoozed'), '0.4.0 1 abc');
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non-numeric level in snooze file → outputs', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
writeFileSync(join(stateDir, 'update-snoozed'), `0.4.0 abc ${nowEpoch()}`);
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('snooze respected on remote fetch path (no cache)', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
|
||||||
|
// No cache file — goes to remote fetch path
|
||||||
|
writeSnooze('0.4.0', 1, nowEpoch() - 3600); // 1h ago
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('');
|
||||||
|
// Cache should still be written
|
||||||
|
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
|
||||||
|
expect(cache).toContain('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('just-upgraded clears snooze file', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n');
|
||||||
|
writeFileSync(join(stateDir, 'just-upgraded-from'), '0.3.3\n');
|
||||||
|
writeSnooze('0.4.0', 2, nowEpoch() - 3600);
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('JUST_UPGRADED 0.3.3 0.4.0');
|
||||||
|
expect(existsSync(join(stateDir, 'update-snoozed'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Config tests ──────────────────────────────────────────
|
||||||
|
test('update_check: false disables all checks', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
|
||||||
|
writeConfig('update_check: false\n');
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('');
|
||||||
|
// No cache should be written
|
||||||
|
expect(existsSync(join(stateDir, 'last-update-check'))).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing config.yaml does not crash', () => {
|
||||||
|
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||||
|
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
|
||||||
|
// No config file — should behave normally
|
||||||
|
|
||||||
|
const { exitCode, stdout } = run();
|
||||||
|
expect(exitCode).toBe(0);
|
||||||
|
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: gstack-upgrade
|
name: gstack-upgrade
|
||||||
version: 1.0.0
|
version: 1.1.0
|
||||||
description: |
|
description: |
|
||||||
Upgrade gstack to the latest version. Detects global vs vendored install,
|
Upgrade gstack to the latest version. Detects global vs vendored install,
|
||||||
runs the upgrade, and shows what's new.
|
runs the upgrade, and shows what's new.
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
|
- Write
|
||||||
- AskUserQuestion
|
- AskUserQuestion
|
||||||
---
|
---
|
||||||
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
||||||
@@ -20,13 +21,56 @@ Upgrade gstack to the latest version and show what's new.
|
|||||||
|
|
||||||
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
|
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
|
||||||
|
|
||||||
### Step 1: Ask the user
|
### Step 1: Ask the user (or auto-upgrade)
|
||||||
|
|
||||||
Use AskUserQuestion:
|
First, check if auto-upgrade is enabled:
|
||||||
- Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now? Takes ~10 seconds."
|
```bash
|
||||||
- Options: ["Yes, upgrade now", "Later (ask again tomorrow)"]
|
_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 "Later":** Run `touch ~/.gstack/last-update-check` to reset the 24h timer and continue with the current skill. Do not mention the upgrade again.
|
**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=~/.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
|
### Step 2: Detect install type
|
||||||
|
|
||||||
@@ -79,12 +123,40 @@ cd "$INSTALL_DIR" && ./setup
|
|||||||
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
|
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Step 4.5: Sync local vendored copy
|
||||||
|
|
||||||
|
After upgrading the primary install, check if there's also a local copy in the current project that needs updating:
|
||||||
|
|
||||||
|
```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
|
||||||
|
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
|
||||||
|
```
|
||||||
|
|
||||||
|
If `LOCAL_GSTACK` is non-empty, 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."
|
||||||
|
|
||||||
### Step 5: Write marker + clear cache
|
### Step 5: Write marker + clear cache
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.gstack
|
mkdir -p ~/.gstack
|
||||||
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
|
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
|
||||||
rm -f ~/.gstack/last-update-check
|
rm -f ~/.gstack/last-update-check
|
||||||
|
rm -f ~/.gstack/update-snoozed
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 6: Show What's New
|
### Step 6: Show What's New
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: gstack-upgrade
|
name: gstack-upgrade
|
||||||
version: 1.0.0
|
version: 1.1.0
|
||||||
description: |
|
description: |
|
||||||
Upgrade gstack to the latest version. Detects global vs vendored install,
|
Upgrade gstack to the latest version. Detects global vs vendored install,
|
||||||
runs the upgrade, and shows what's new.
|
runs the upgrade, and shows what's new.
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
|
- Write
|
||||||
- AskUserQuestion
|
- AskUserQuestion
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -18,13 +19,56 @@ Upgrade gstack to the latest version and show what's new.
|
|||||||
|
|
||||||
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
|
This section is referenced by all skill preambles when they detect `UPGRADE_AVAILABLE`.
|
||||||
|
|
||||||
### Step 1: Ask the user
|
### Step 1: Ask the user (or auto-upgrade)
|
||||||
|
|
||||||
Use AskUserQuestion:
|
First, check if auto-upgrade is enabled:
|
||||||
- Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now? Takes ~10 seconds."
|
```bash
|
||||||
- Options: ["Yes, upgrade now", "Later (ask again tomorrow)"]
|
_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 "Later":** Run `touch ~/.gstack/last-update-check` to reset the 24h timer and continue with the current skill. Do not mention the upgrade again.
|
**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=~/.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
|
### Step 2: Detect install type
|
||||||
|
|
||||||
@@ -77,12 +121,40 @@ cd "$INSTALL_DIR" && ./setup
|
|||||||
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
|
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Step 4.5: Sync local vendored copy
|
||||||
|
|
||||||
|
After upgrading the primary install, check if there's also a local copy in the current project that needs updating:
|
||||||
|
|
||||||
|
```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
|
||||||
|
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
|
||||||
|
```
|
||||||
|
|
||||||
|
If `LOCAL_GSTACK` is non-empty, 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."
|
||||||
|
|
||||||
### Step 5: Write marker + clear cache
|
### Step 5: Write marker + clear cache
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p ~/.gstack
|
mkdir -p ~/.gstack
|
||||||
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
|
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
|
||||||
rm -f ~/.gstack/last-update-check
|
rm -f ~/.gstack/last-update-check
|
||||||
|
rm -f ~/.gstack/update-snoozed
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 6: Show What's New
|
### Step 6: Show What's New
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
|
|||||||
[ -n "$_UPD" ] && echo "$_UPD" || true
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||||
|
|
||||||
# Mega Plan Review Mode
|
# Mega Plan Review Mode
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
|
|||||||
[ -n "$_UPD" ] && echo "$_UPD" || true
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||||
|
|
||||||
# Plan Review Mode
|
# Plan Review Mode
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
|
|||||||
[ -n "$_UPD" ] && echo "$_UPD" || true
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||||
|
|
||||||
# /qa: Systematic QA Testing
|
# /qa: Systematic QA Testing
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
|
|||||||
[ -n "$_UPD" ] && echo "$_UPD" || true
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||||
|
|
||||||
# /retro — Weekly Engineering Retrospective
|
# /retro — Weekly Engineering Retrospective
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
|
|||||||
[ -n "$_UPD" ] && echo "$_UPD" || true
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||||
|
|
||||||
# Pre-Landing PR Review
|
# Pre-Landing PR Review
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
|
|||||||
[ -n "$_UPD" ] && echo "$_UPD" || true
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
If output shows \`UPGRADE_AVAILABLE <old> <new>\`: read \`~/.claude/skills/gstack/gstack-upgrade/SKILL.md\` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, \`touch ~/.gstack/last-update-check\` if no). If \`JUST_UPGRADED <from> <to>\`: tell user "Running gstack v{to} (just updated!)" and continue.`;
|
If output shows \`UPGRADE_AVAILABLE <old> <new>\`: read \`~/.claude/skills/gstack/gstack-upgrade/SKILL.md\` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If \`JUST_UPGRADED <from> <to>\`: tell user "Running gstack v{to} (just updated!)" and continue.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateBrowseSetup(): string {
|
function generateBrowseSetup(): string {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
|
|||||||
[ -n "$_UPD" ] && echo "$_UPD" || true
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||||
|
|
||||||
# Setup Browser Cookies
|
# Setup Browser Cookies
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ _UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/sk
|
|||||||
[ -n "$_UPD" ] && echo "$_UPD" || true
|
[ -n "$_UPD" ] && echo "$_UPD" || true
|
||||||
```
|
```
|
||||||
|
|
||||||
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (AskUserQuestion → upgrade if yes, `touch ~/.gstack/last-update-check` if no). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
If output shows `UPGRADE_AVAILABLE <old> <new>`: read `~/.claude/skills/gstack/gstack-upgrade/SKILL.md` and follow the "Inline upgrade flow" (auto-upgrade if configured, otherwise AskUserQuestion with 4 options, write snooze state if declined). If `JUST_UPGRADED <from> <to>`: tell user "Running gstack v{to} (just updated!)" and continue.
|
||||||
|
|
||||||
# Ship: Fully Automated Ship Workflow
|
# Ship: Fully Automated Ship Workflow
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user