mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
feat: daily update check + /gstack-upgrade skill (v0.3.4) (#42)
* feat: add daily update check script + /gstack-upgrade skill bin/gstack-update-check: pure bash, checks VERSION against remote once/day, outputs UPGRADE_AVAILABLE or JUST_UPGRADED. Uses ~/.gstack/ for state. gstack-upgrade/SKILL.md: new skill with inline upgrade flow for all preambles. Detects global-git, local-git, vendored installs. Shows What's New from CHANGELOG. browse/test/gstack-update-check.test.ts: 10 test cases covering all branch paths. * refactor: remove version check from find-browse, simplify to binary locator Delete checkVersion(), readCache(), writeCache(), fetchRemoteSHA(), resolveSkillDir(), CacheEntry interface, REPO_URL/CACHE_PATH/CACHE_TTL constants, and META output from find-browse.ts. Version checking is now handled by bin/gstack-update-check (previous commit). * feat: add update check preamble to all 9 skills Every skill now runs bin/gstack-update-check on invocation. If an upgrade is available, reads gstack-upgrade/SKILL.md inline upgrade flow. Also adds AskUserQuestion to 5 skills that lacked it (gstack root, browse, qa, retro, setup-browser-cookies) and Bash to plan-eng-review. Simplifies qa and setup-browser-cookies setup blocks (removes META parsing). * chore: bump version and changelog (v0.3.4) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove unused import + add corrupt cache test Address pre-landing review findings: - Remove unused mkdirSync import from gstack-update-check.test.ts - Add Path I test: corrupt cache file falls through to remote fetch 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:
@@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.4 — 2026-03-13
|
||||
|
||||
### Added
|
||||
- **Daily update check** — all 9 skills now check for new versions once per day via `bin/gstack-update-check` (pure bash, <5ms cached). Prompts user via AskUserQuestion with option to upgrade or defer 24h.
|
||||
- **`/gstack-upgrade` skill** — standalone upgrade command that detects install type (global-git, local-git, vendored), upgrades, and shows a "What's New" summary from CHANGELOG
|
||||
- **"Just upgraded" confirmation** — after upgrading, the next skill invocation shows "Running gstack v{new} (just updated!)" via `~/.gstack/just-upgraded-from` marker
|
||||
- **`AskUserQuestion` added to 5 skills** — gstack (root), browse, qa, retro, setup-browser-cookies now have AskUserQuestion in allowed-tools for upgrade prompts
|
||||
- **`Bash` added to plan-eng-review** — enables the update check preamble to run in plan review sessions
|
||||
- `browse/test/gstack-update-check.test.ts` — 10 test cases covering all script branch paths with `GSTACK_REMOTE_URL` env var for test isolation
|
||||
- `TODOS.md` for tracking deferred work
|
||||
|
||||
### Changed
|
||||
- **Version check is now one system** — removed SHA-based `checkVersion()` from `browse/src/find-browse.ts` (~120 lines deleted) and `browse/test/find-browse.test.ts` (~100 lines deleted). Replaced by `bin/gstack-update-check` bash script using semver VERSION comparison with 24h cache.
|
||||
- Simplified `qa/SKILL.md` and `setup-browser-cookies/SKILL.md` setup blocks — removed old `BROWSE_OUTPUT`/`META` parsing, now use simple `find-browse` call
|
||||
- Updated `browse/bin/find-browse` shim comments to reflect simplified role (binary locator only)
|
||||
|
||||
### Removed
|
||||
- `checkVersion()`, `readCache()`, `writeCache()`, `fetchRemoteSHA()`, `resolveSkillDir()`, `CacheEntry` interface from `browse/src/find-browse.ts`
|
||||
- `META:UPDATE_AVAILABLE` protocol from find-browse output
|
||||
- Old META-based upgrade instructions from qa and setup-browser-cookies SKILL.md files
|
||||
- Legacy `/tmp/gstack-latest-version` cache file (cleaned up by `setup` script)
|
||||
|
||||
## Unreleased — 2026-03-14
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -10,11 +10,21 @@ description: |
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- AskUserQuestion
|
||||
|
||||
---
|
||||
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# gstack browse: QA Testing & Dogfooding
|
||||
|
||||
Persistent headless Chromium. First call auto-starts (~3s), then ~100-200ms per command.
|
||||
|
||||
@@ -10,9 +10,19 @@ description: |
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- AskUserQuestion
|
||||
|
||||
---
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# gstack browse: QA Testing & Dogfooding
|
||||
|
||||
Persistent headless Chromium. First call auto-starts (~3s), then ~100-200ms per command.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# TODOS
|
||||
|
||||
## Auto-upgrade mode (zero-prompt)
|
||||
|
||||
**What:** Add a `GSTACK_AUTO_UPGRADE=1` env var or `~/.gstack/config` option that skips the AskUserQuestion prompt and upgrades automatically when a new version is detected.
|
||||
|
||||
**Why:** Power users and CI environments may want zero-friction upgrades without being asked every time.
|
||||
|
||||
**Context:** The current upgrade system (v0.3.4) always prompts via AskUserQuestion. This TODO adds an opt-in bypass. Implementation is ~10 lines in the preamble instructions: check for the env var/config before calling AskUserQuestion, and if set, go straight to the upgrade flow. Depends on the full upgrade system being stable first — wait for user feedback on the prompt-based flow before adding this.
|
||||
|
||||
**Effort:** S (small)
|
||||
**Priority:** P3 (nice-to-have, revisit after adoption data)
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
# gstack-update-check — daily version check for all skills.
|
||||
#
|
||||
# Output (one line, or nothing):
|
||||
# JUST_UPGRADED <old> <new> — marker found from recent upgrade
|
||||
# UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
|
||||
# (nothing) — up to date or check skipped
|
||||
#
|
||||
# Env overrides (for testing):
|
||||
# GSTACK_DIR — override auto-detected gstack root
|
||||
# GSTACK_REMOTE_URL — override remote VERSION URL
|
||||
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
||||
set -euo pipefail
|
||||
|
||||
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
||||
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
||||
CACHE_FILE="$STATE_DIR/last-update-check"
|
||||
MARKER_FILE="$STATE_DIR/just-upgraded-from"
|
||||
VERSION_FILE="$GSTACK_DIR/VERSION"
|
||||
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
|
||||
|
||||
# ─── Step 1: Read local version ──────────────────────────────
|
||||
LOCAL=""
|
||||
if [ -f "$VERSION_FILE" ]; then
|
||||
LOCAL="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')"
|
||||
fi
|
||||
if [ -z "$LOCAL" ]; then
|
||||
exit 0 # No VERSION file → skip check
|
||||
fi
|
||||
|
||||
# ─── Step 2: Check "just upgraded" marker ─────────────────────
|
||||
if [ -f "$MARKER_FILE" ]; then
|
||||
OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')"
|
||||
rm -f "$MARKER_FILE"
|
||||
mkdir -p "$STATE_DIR"
|
||||
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
||||
if [ -n "$OLD" ]; then
|
||||
echo "JUST_UPGRADED $OLD $LOCAL"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ─── Step 3: Check cache freshness (24h = 1440 min) ──────────
|
||||
if [ -f "$CACHE_FILE" ]; then
|
||||
# Cache is fresh if modified within 1440 minutes
|
||||
STALE=$(find "$CACHE_FILE" -mmin +1440 2>/dev/null || true)
|
||||
if [ -z "$STALE" ]; then
|
||||
# Cache is fresh — read it
|
||||
CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)"
|
||||
case "$CACHED" in
|
||||
UP_TO_DATE*)
|
||||
exit 0
|
||||
;;
|
||||
UPGRADE_AVAILABLE*)
|
||||
# Verify local version still matches cached old version
|
||||
CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')"
|
||||
if [ "$CACHED_OLD" = "$LOCAL" ]; then
|
||||
echo "$CACHED"
|
||||
exit 0
|
||||
fi
|
||||
# Local version changed (manual upgrade?) — fall through to re-check
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── Step 4: Slow path — fetch remote version ────────────────
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
REMOTE=""
|
||||
REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)"
|
||||
REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
|
||||
|
||||
# Validate: must look like a version number (reject HTML error pages)
|
||||
if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+$'; then
|
||||
# Invalid or empty response — assume up to date
|
||||
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$LOCAL" = "$REMOTE" ]; then
|
||||
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Versions differ — upgrade available
|
||||
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
|
||||
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"
|
||||
@@ -10,11 +10,21 @@ description: |
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- AskUserQuestion
|
||||
|
||||
---
|
||||
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# browse: QA Testing & Dogfooding
|
||||
|
||||
Persistent headless Chromium. First call auto-starts (~3s), then ~100ms per command.
|
||||
|
||||
@@ -10,9 +10,19 @@ description: |
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- AskUserQuestion
|
||||
|
||||
---
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# browse: QA Testing & Dogfooding
|
||||
|
||||
Persistent headless Chromium. First call auto-starts (~3s), then ~100ms per command.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/bash
|
||||
# Shim: delegates to compiled find-browse binary, falls back to basic discovery.
|
||||
# The compiled binary adds version checking and META signal support.
|
||||
# The compiled binary handles git root detection for workspace-local installs.
|
||||
DIR="$(cd "$(dirname "$0")/.." && pwd)/dist"
|
||||
if test -x "$DIR/find-browse"; then
|
||||
exec "$DIR/find-browse" "$@"
|
||||
fi
|
||||
# Fallback: basic discovery (no version check)
|
||||
# Fallback: basic discovery
|
||||
ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
if [ -n "$ROOT" ] && test -x "$ROOT/.claude/skills/gstack/browse/dist/browse"; then
|
||||
echo "$ROOT/.claude/skills/gstack/browse/dist/browse"
|
||||
|
||||
+3
-128
@@ -1,28 +1,14 @@
|
||||
/**
|
||||
* find-browse — locate the gstack browse binary + check for updates.
|
||||
* find-browse — locate the gstack browse binary.
|
||||
*
|
||||
* Compiled to browse/dist/find-browse (standalone binary, no bun runtime needed).
|
||||
*
|
||||
* Output protocol:
|
||||
* Line 1: /path/to/binary (always present)
|
||||
* Line 2+: META:<TYPE> <json-payload> (optional, 0 or more)
|
||||
*
|
||||
* META types:
|
||||
* META:UPDATE_AVAILABLE — local binary is behind origin/main
|
||||
*
|
||||
* All version checks are best-effort: network failures, missing files, and
|
||||
* cache errors degrade gracefully to outputting only the binary path.
|
||||
* Outputs the absolute path to the browse binary on stdout, or exits 1 if not found.
|
||||
*/
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import { readFileSync, writeFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
const REPO_URL = 'https://github.com/garrytan/gstack.git';
|
||||
const CACHE_PATH = '/tmp/gstack-latest-version';
|
||||
const CACHE_TTL = 14400; // 4 hours in seconds
|
||||
|
||||
// ─── Binary Discovery ───────────────────────────────────────────
|
||||
|
||||
function getGitRoot(): string | null {
|
||||
@@ -55,114 +41,6 @@ export function locateBinary(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Version Check ──────────────────────────────────────────────
|
||||
|
||||
interface CacheEntry {
|
||||
sha: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
function readCache(): CacheEntry | null {
|
||||
try {
|
||||
const content = readFileSync(CACHE_PATH, 'utf-8').trim();
|
||||
const parts = content.split(/\s+/);
|
||||
if (parts.length < 2) return null;
|
||||
const sha = parts[0];
|
||||
const timestamp = parseInt(parts[1], 10);
|
||||
if (!sha || isNaN(timestamp)) return null;
|
||||
// Validate SHA is hex
|
||||
if (!/^[0-9a-f]{40}$/i.test(sha)) return null;
|
||||
return { sha, timestamp };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(sha: string, timestamp: number): void {
|
||||
try {
|
||||
writeFileSync(CACHE_PATH, `${sha} ${timestamp}\n`);
|
||||
} catch {
|
||||
// Cache write failure is non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
function fetchRemoteSHA(): string | null {
|
||||
try {
|
||||
const proc = Bun.spawnSync(['git', 'ls-remote', REPO_URL, 'refs/heads/main'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
timeout: 10_000, // 10s timeout
|
||||
});
|
||||
if (proc.exitCode !== 0) return null;
|
||||
const output = proc.stdout.toString().trim();
|
||||
const sha = output.split(/\s+/)[0];
|
||||
if (!sha || !/^[0-9a-f]{40}$/i.test(sha)) return null;
|
||||
return sha;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSkillDir(binaryPath: string): string | null {
|
||||
const home = homedir();
|
||||
const globalPrefix = join(home, '.claude', 'skills', 'gstack');
|
||||
if (binaryPath.startsWith(globalPrefix)) return globalPrefix;
|
||||
|
||||
// Workspace-local: binary is at $ROOT/.claude/skills/gstack/browse/dist/browse
|
||||
// Skill dir is $ROOT/.claude/skills/gstack
|
||||
const parts = binaryPath.split('/.claude/skills/gstack/');
|
||||
if (parts.length === 2) return parts[0] + '/.claude/skills/gstack';
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function checkVersion(binaryDir: string): string | null {
|
||||
// Read local version
|
||||
const versionFile = join(binaryDir, '.version');
|
||||
let localSHA: string;
|
||||
try {
|
||||
localSHA = readFileSync(versionFile, 'utf-8').trim();
|
||||
} catch {
|
||||
return null; // No .version file → skip check
|
||||
}
|
||||
if (!localSHA) return null;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Check cache
|
||||
let remoteSHA: string | null = null;
|
||||
const cache = readCache();
|
||||
if (cache && (now - cache.timestamp) < CACHE_TTL) {
|
||||
remoteSHA = cache.sha;
|
||||
}
|
||||
|
||||
// Fetch from remote if cache miss
|
||||
if (!remoteSHA) {
|
||||
remoteSHA = fetchRemoteSHA();
|
||||
if (remoteSHA) {
|
||||
writeCache(remoteSHA, now);
|
||||
}
|
||||
}
|
||||
|
||||
if (!remoteSHA) return null; // Offline or error → skip check
|
||||
|
||||
// Compare
|
||||
if (localSHA === remoteSHA) return null; // Up to date
|
||||
|
||||
// Determine skill directory for update command
|
||||
const binaryPath = join(binaryDir, 'browse');
|
||||
const skillDir = resolveSkillDir(binaryPath);
|
||||
if (!skillDir) return null;
|
||||
|
||||
const payload = JSON.stringify({
|
||||
current: localSHA.slice(0, 8),
|
||||
latest: remoteSHA.slice(0, 8),
|
||||
command: `cd ${skillDir} && git stash && git fetch origin && git reset --hard origin/main && ./setup`,
|
||||
});
|
||||
|
||||
return `META:UPDATE_AVAILABLE ${payload}`;
|
||||
}
|
||||
|
||||
// ─── Main ───────────────────────────────────────────────────────
|
||||
|
||||
function main() {
|
||||
@@ -173,9 +51,6 @@ function main() {
|
||||
}
|
||||
|
||||
console.log(bin);
|
||||
|
||||
const meta = checkVersion(dirname(bin));
|
||||
if (meta) console.log(meta);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
@@ -1,130 +1,10 @@
|
||||
/**
|
||||
* Tests for find-browse version check logic
|
||||
*
|
||||
* Tests the checkVersion() and locateBinary() functions directly.
|
||||
* Uses temp directories with mock .version files and cache files.
|
||||
* Tests for find-browse binary locator.
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { checkVersion, locateBinary } from '../src/find-browse';
|
||||
import { mkdtempSync, writeFileSync, rmSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), 'find-browse-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
// Clean up test cache
|
||||
try { rmSync('/tmp/gstack-latest-version'); } catch {}
|
||||
});
|
||||
|
||||
describe('checkVersion', () => {
|
||||
test('returns null when .version file is missing', () => {
|
||||
const result = checkVersion(tempDir);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when .version file is empty', () => {
|
||||
writeFileSync(join(tempDir, '.version'), '');
|
||||
const result = checkVersion(tempDir);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when .version has only whitespace', () => {
|
||||
writeFileSync(join(tempDir, '.version'), ' \n');
|
||||
const result = checkVersion(tempDir);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns null when local SHA matches remote (cache hit)', () => {
|
||||
const sha = 'a'.repeat(40);
|
||||
writeFileSync(join(tempDir, '.version'), sha);
|
||||
// Write cache with same SHA, recent timestamp
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
writeFileSync('/tmp/gstack-latest-version', `${sha} ${now}\n`);
|
||||
|
||||
const result = checkVersion(tempDir);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('returns META:UPDATE_AVAILABLE when SHAs differ (cache hit)', () => {
|
||||
const localSha = 'a'.repeat(40);
|
||||
const remoteSha = 'b'.repeat(40);
|
||||
writeFileSync(join(tempDir, '.version'), localSha);
|
||||
// Create a fake browse binary path so resolveSkillDir works
|
||||
const browsePath = join(tempDir, 'browse');
|
||||
writeFileSync(browsePath, '');
|
||||
// Write cache with different SHA, recent timestamp
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
writeFileSync('/tmp/gstack-latest-version', `${remoteSha} ${now}\n`);
|
||||
|
||||
const result = checkVersion(tempDir);
|
||||
// Result may be null if resolveSkillDir can't determine skill dir from temp path
|
||||
// That's expected — the META signal requires a known skill dir path
|
||||
if (result !== null) {
|
||||
expect(result).toStartWith('META:UPDATE_AVAILABLE');
|
||||
const jsonStr = result.replace('META:UPDATE_AVAILABLE ', '');
|
||||
const payload = JSON.parse(jsonStr);
|
||||
expect(payload.current).toBe('a'.repeat(8));
|
||||
expect(payload.latest).toBe('b'.repeat(8));
|
||||
expect(payload.command).toContain('git stash');
|
||||
expect(payload.command).toContain('git reset --hard origin/main');
|
||||
expect(payload.command).toContain('./setup');
|
||||
}
|
||||
});
|
||||
|
||||
test('uses cached SHA when cache is fresh (< 4hr)', () => {
|
||||
const localSha = 'a'.repeat(40);
|
||||
const remoteSha = 'a'.repeat(40);
|
||||
writeFileSync(join(tempDir, '.version'), localSha);
|
||||
// Cache is 1 hour old — should still be valid
|
||||
const oneHourAgo = Math.floor(Date.now() / 1000) - 3600;
|
||||
writeFileSync('/tmp/gstack-latest-version', `${remoteSha} ${oneHourAgo}\n`);
|
||||
|
||||
const result = checkVersion(tempDir);
|
||||
expect(result).toBeNull(); // SHAs match
|
||||
});
|
||||
|
||||
test('treats expired cache as stale', () => {
|
||||
const localSha = 'a'.repeat(40);
|
||||
writeFileSync(join(tempDir, '.version'), localSha);
|
||||
// Cache is 5 hours old — should be stale
|
||||
const fiveHoursAgo = Math.floor(Date.now() / 1000) - 18000;
|
||||
writeFileSync('/tmp/gstack-latest-version', `${'b'.repeat(40)} ${fiveHoursAgo}\n`);
|
||||
|
||||
// This will try git ls-remote which may fail in test env — that's OK
|
||||
// The important thing is it doesn't use the stale cache value
|
||||
const result = checkVersion(tempDir);
|
||||
// Result depends on whether git ls-remote succeeds in test environment
|
||||
// If offline, returns null (graceful degradation)
|
||||
expect(result === null || typeof result === 'string').toBe(true);
|
||||
});
|
||||
|
||||
test('handles corrupt cache file gracefully', () => {
|
||||
const localSha = 'a'.repeat(40);
|
||||
writeFileSync(join(tempDir, '.version'), localSha);
|
||||
writeFileSync('/tmp/gstack-latest-version', 'garbage data here');
|
||||
|
||||
// Should not throw, should treat as stale
|
||||
const result = checkVersion(tempDir);
|
||||
expect(result === null || typeof result === 'string').toBe(true);
|
||||
});
|
||||
|
||||
test('handles cache with invalid SHA gracefully', () => {
|
||||
const localSha = 'a'.repeat(40);
|
||||
writeFileSync(join(tempDir, '.version'), localSha);
|
||||
writeFileSync('/tmp/gstack-latest-version', `not-a-sha ${Math.floor(Date.now() / 1000)}\n`);
|
||||
|
||||
// Invalid SHA should be treated as no cache
|
||||
const result = checkVersion(tempDir);
|
||||
expect(result === null || typeof result === 'string').toBe(true);
|
||||
});
|
||||
});
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { locateBinary } from '../src/find-browse';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
describe('locateBinary', () => {
|
||||
test('returns null when no binary exists at known paths', () => {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Tests for bin/gstack-update-check bash script.
|
||||
*
|
||||
* Uses Bun.spawnSync to invoke the script with temp dirs and
|
||||
* GSTACK_DIR / GSTACK_STATE_DIR / GSTACK_REMOTE_URL env overrides
|
||||
* for full isolation.
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
const SCRIPT = join(import.meta.dir, '..', '..', 'bin', 'gstack-update-check');
|
||||
|
||||
let gstackDir: string;
|
||||
let stateDir: string;
|
||||
|
||||
function run(extraEnv: Record<string, string> = {}) {
|
||||
const result = Bun.spawnSync(['bash', SCRIPT], {
|
||||
env: {
|
||||
...process.env,
|
||||
GSTACK_DIR: gstackDir,
|
||||
GSTACK_STATE_DIR: stateDir,
|
||||
GSTACK_REMOTE_URL: `file://${join(gstackDir, 'REMOTE_VERSION')}`,
|
||||
...extraEnv,
|
||||
},
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
});
|
||||
return {
|
||||
exitCode: result.exitCode,
|
||||
stdout: result.stdout.toString().trim(),
|
||||
stderr: result.stderr.toString().trim(),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
gstackDir = mkdtempSync(join(tmpdir(), 'gstack-upd-test-'));
|
||||
stateDir = mkdtempSync(join(tmpdir(), 'gstack-state-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(gstackDir, { recursive: true, force: true });
|
||||
rmSync(stateDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('gstack-update-check', () => {
|
||||
// ─── Path A: No VERSION file ────────────────────────────────
|
||||
test('exits 0 with no output when VERSION file is missing', () => {
|
||||
const { exitCode, stdout } = run();
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe('');
|
||||
});
|
||||
|
||||
// ─── Path B: Empty VERSION file ─────────────────────────────
|
||||
test('exits 0 with no output when VERSION file is empty', () => {
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '');
|
||||
const { exitCode, stdout } = run();
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe('');
|
||||
});
|
||||
|
||||
// ─── Path C: Just-upgraded marker ───────────────────────────
|
||||
test('outputs JUST_UPGRADED and deletes marker', () => {
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n');
|
||||
writeFileSync(join(stateDir, 'just-upgraded-from'), '0.3.3\n');
|
||||
|
||||
const { exitCode, stdout } = run();
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe('JUST_UPGRADED 0.3.3 0.4.0');
|
||||
// Marker should be deleted
|
||||
expect(existsSync(join(stateDir, 'just-upgraded-from'))).toBe(false);
|
||||
// Cache should be written
|
||||
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
|
||||
expect(cache).toContain('UP_TO_DATE');
|
||||
});
|
||||
|
||||
// ─── Path D1: Fresh cache, UP_TO_DATE ───────────────────────
|
||||
test('exits silently when cache says UP_TO_DATE and is fresh', () => {
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||
writeFileSync(join(stateDir, 'last-update-check'), 'UP_TO_DATE 0.3.3');
|
||||
|
||||
const { exitCode, stdout } = run();
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe('');
|
||||
});
|
||||
|
||||
// ─── Path D2: Fresh cache, UPGRADE_AVAILABLE ────────────────
|
||||
test('echoes cached UPGRADE_AVAILABLE when cache is fresh', () => {
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||
|
||||
const { exitCode, stdout } = run();
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||
});
|
||||
|
||||
// ─── Path D3: Fresh cache, but local version changed ────────
|
||||
test('re-checks when local version does not match cached old version', () => {
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '0.4.0\n');
|
||||
// Cache says 0.3.3 → 0.4.0 but we're already on 0.4.0
|
||||
writeFileSync(join(stateDir, 'last-update-check'), 'UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||
// Remote also says 0.4.0 — should be up to date
|
||||
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
|
||||
|
||||
const { exitCode, stdout } = run();
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe(''); // Up to date after re-check
|
||||
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
|
||||
expect(cache).toContain('UP_TO_DATE');
|
||||
});
|
||||
|
||||
// ─── Path E: Versions match (remote fetch) ─────────────────
|
||||
test('writes UP_TO_DATE cache when versions match', () => {
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n');
|
||||
|
||||
const { exitCode, stdout } = run();
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe('');
|
||||
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
|
||||
expect(cache).toContain('UP_TO_DATE');
|
||||
});
|
||||
|
||||
// ─── Path F: Versions differ (remote fetch) ─────────────────
|
||||
test('outputs UPGRADE_AVAILABLE when versions differ', () => {
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.4.0\n');
|
||||
|
||||
const { exitCode, stdout } = run();
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
|
||||
expect(cache).toContain('UPGRADE_AVAILABLE 0.3.3 0.4.0');
|
||||
});
|
||||
|
||||
// ─── Path G: Invalid remote response ────────────────────────
|
||||
test('treats invalid remote response as up to date', () => {
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '<html>404 Not Found</html>\n');
|
||||
|
||||
const { exitCode, stdout } = run();
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe('');
|
||||
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
|
||||
expect(cache).toContain('UP_TO_DATE');
|
||||
});
|
||||
|
||||
// ─── Path H: Curl fails (bad URL) ──────────────────────────
|
||||
test('exits silently when remote URL is unreachable', () => {
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||
|
||||
const { exitCode, stdout } = run({
|
||||
GSTACK_REMOTE_URL: 'file:///nonexistent/path/VERSION',
|
||||
});
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe('');
|
||||
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
|
||||
expect(cache).toContain('UP_TO_DATE');
|
||||
});
|
||||
|
||||
// ─── Path I: Corrupt cache file ─────────────────────────────
|
||||
test('falls through to remote fetch when cache is corrupt', () => {
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||
writeFileSync(join(stateDir, 'last-update-check'), 'garbage data here');
|
||||
// Remote says same version — should end up UP_TO_DATE
|
||||
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n');
|
||||
|
||||
const { exitCode, stdout } = run();
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toBe('');
|
||||
// Cache should be overwritten with valid content
|
||||
const cache = readFileSync(join(stateDir, 'last-update-check'), 'utf-8');
|
||||
expect(cache).toContain('UP_TO_DATE');
|
||||
});
|
||||
|
||||
// ─── State dir creation ─────────────────────────────────────
|
||||
test('creates state dir if it does not exist', () => {
|
||||
const newStateDir = join(stateDir, 'nested', 'dir');
|
||||
writeFileSync(join(gstackDir, 'VERSION'), '0.3.3\n');
|
||||
writeFileSync(join(gstackDir, 'REMOTE_VERSION'), '0.3.3\n');
|
||||
|
||||
const { exitCode } = run({ GSTACK_STATE_DIR: newStateDir });
|
||||
expect(exitCode).toBe(0);
|
||||
expect(existsSync(join(newStateDir, 'last-update-check'))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: gstack-upgrade
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Upgrade gstack to the latest version. Detects global vs vendored install,
|
||||
runs the upgrade, and shows what's new.
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- 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
|
||||
|
||||
Use AskUserQuestion:
|
||||
- Question: "gstack **v{new}** is available (you're on v{old}). Upgrade now? Takes ~10 seconds."
|
||||
- Options: ["Yes, upgrade now", "Later (ask again tomorrow)"]
|
||||
|
||||
**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.
|
||||
|
||||
### 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 ".claude/skills/gstack/.git" ]; then
|
||||
INSTALL_TYPE="local-git"
|
||||
INSTALL_DIR=".claude/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"
|
||||
```
|
||||
|
||||
### Step 3: Save old version
|
||||
|
||||
```bash
|
||||
OLD_VERSION=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
||||
```
|
||||
|
||||
### Step 4: Upgrade
|
||||
|
||||
**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 5: Write marker + clear cache
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.gstack
|
||||
echo "$OLD_VERSION" > ~/.gstack/just-upgraded-from
|
||||
rm -f ~/.gstack/last-update-check
|
||||
```
|
||||
|
||||
### 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), follow Steps 2-6 above. If already on the latest version, tell the user: "You're already on the latest version (v{version})."
|
||||
@@ -14,6 +14,15 @@ allowed-tools:
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# Mega Plan Review Mode
|
||||
|
||||
## Philosophy
|
||||
|
||||
@@ -10,8 +10,18 @@ allowed-tools:
|
||||
- Grep
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
- Bash
|
||||
---
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# Plan Review Mode
|
||||
|
||||
Review this plan thoroughly before making any code changes. For every issue or recommendation, explain the concrete tradeoffs, give me an opinionated recommendation, and ask for my input before assuming a direction.
|
||||
|
||||
+19
-9
@@ -11,8 +11,18 @@ allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# /qa: Systematic QA Testing
|
||||
|
||||
You are a QA engineer. Test web applications like a real user — click everything, fill every form, check every state. Produce a structured report with evidence.
|
||||
@@ -34,18 +44,18 @@ You are a QA engineer. Test web applications like a real user — click everythi
|
||||
**Find the browse binary:**
|
||||
|
||||
```bash
|
||||
BROWSE_OUTPUT=$(browse/bin/find-browse 2>/dev/null || ~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null)
|
||||
B=$(echo "$BROWSE_OUTPUT" | head -1)
|
||||
META=$(echo "$BROWSE_OUTPUT" | grep "^META:" || true)
|
||||
if [ -z "$B" ]; then
|
||||
echo "ERROR: browse binary not found"
|
||||
exit 1
|
||||
B=$(browse/bin/find-browse 2>/dev/null || ~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null)
|
||||
if [ -n "$B" ]; then
|
||||
echo "READY: $B"
|
||||
else
|
||||
echo "NEEDS_SETUP"
|
||||
fi
|
||||
echo "READY: $B"
|
||||
[ -n "$META" ] && echo "$META"
|
||||
```
|
||||
|
||||
If you see `META:UPDATE_AVAILABLE`: tell the user an update is available, STOP and wait for approval, then run the command from the META payload and re-run the setup check.
|
||||
If `NEEDS_SETUP`:
|
||||
1. Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait.
|
||||
2. Run: `cd <SKILL_DIR> && ./setup`
|
||||
3. If `bun` is not installed: `curl -fsSL https://bun.sh/install | bash`
|
||||
|
||||
**Create output directories:**
|
||||
|
||||
|
||||
@@ -10,8 +10,18 @@ allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# /retro — Weekly Engineering Retrospective
|
||||
|
||||
Generates a comprehensive engineering retrospective analyzing commit history, work patterns, and code quality metrics. Team-aware: identifies the user running the command, then analyzes every contributor with per-person praise and growth opportunities. Designed for a senior IC/CTO-level builder using Claude Code as a force multiplier.
|
||||
|
||||
@@ -14,6 +14,15 @@ allowed-tools:
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# Pre-Landing PR Review
|
||||
|
||||
You are running the `/review` workflow. Analyze the current branch's diff against main for structural issues that tests don't catch.
|
||||
|
||||
@@ -85,3 +85,10 @@ else
|
||||
echo " browse: $BROWSE_BIN"
|
||||
echo " (skipped skill symlinks — not inside .claude/skills/)"
|
||||
fi
|
||||
|
||||
# 4. First-time welcome + legacy cleanup
|
||||
if [ ! -d "$HOME/.gstack" ]; then
|
||||
mkdir -p "$HOME/.gstack"
|
||||
echo " Welcome! Run /gstack-upgrade anytime to stay current."
|
||||
fi
|
||||
rm -f /tmp/gstack-latest-version
|
||||
|
||||
@@ -8,8 +8,18 @@ description: |
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# Setup Browser Cookies
|
||||
|
||||
Import logged-in sessions from your real Chromium browser into the headless browse session.
|
||||
@@ -26,12 +36,9 @@ Import logged-in sessions from your real Chromium browser into the headless brow
|
||||
### 1. Find the browse binary
|
||||
|
||||
```bash
|
||||
BROWSE_OUTPUT=$(browse/bin/find-browse 2>/dev/null || ~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null)
|
||||
B=$(echo "$BROWSE_OUTPUT" | head -1)
|
||||
META=$(echo "$BROWSE_OUTPUT" | grep "^META:" || true)
|
||||
B=$(browse/bin/find-browse 2>/dev/null || ~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null)
|
||||
if [ -n "$B" ]; then
|
||||
echo "READY: $B"
|
||||
[ -n "$META" ] && echo "$META"
|
||||
else
|
||||
echo "NEEDS_SETUP"
|
||||
fi
|
||||
@@ -42,8 +49,6 @@ If `NEEDS_SETUP`:
|
||||
2. Run: `cd <SKILL_DIR> && ./setup`
|
||||
3. If `bun` is not installed: `curl -fsSL https://bun.sh/install | bash`
|
||||
|
||||
If you see `META:UPDATE_AVAILABLE`: tell the user an update is available, STOP and wait for approval, then run the command from the META payload and re-run the setup check.
|
||||
|
||||
### 2. Open the cookie picker
|
||||
|
||||
```bash
|
||||
|
||||
@@ -13,6 +13,15 @@ allowed-tools:
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
## Update Check (run first)
|
||||
|
||||
```bash
|
||||
_UPD=$(~/.claude/skills/gstack/bin/gstack-update-check 2>/dev/null || .claude/skills/gstack/bin/gstack-update-check 2>/dev/null || true)
|
||||
[ -n "$_UPD" ] && echo "$_UPD"
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
# Ship: Fully Automated Ship Workflow
|
||||
|
||||
You are running the `/ship` workflow. This is a **non-interactive, fully automated** workflow. Do NOT ask for confirmation at any step. The user said `/ship` which means DO IT. Run straight through and output the PR URL at the end.
|
||||
|
||||
Reference in New Issue
Block a user