mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
Merge remote-tracking branch 'origin/main' into garrytan/security-wave-5
# Conflicts: # CHANGELOG.md
This commit is contained in:
+9
-1
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [0.15.14.0] - 2026-04-06
|
||||
## [0.15.15.0] - 2026-04-06
|
||||
|
||||
Community security wave: 8 PRs from 4 contributors, every fix credited as co-author.
|
||||
|
||||
@@ -36,6 +36,14 @@ Community security wave: 8 PRs from 4 contributors, every fix credited as co-aut
|
||||
- State load filters cookies from localhost, .internal, and metadata domains.
|
||||
- Telemetry sync logs upsert errors from installation tracking.
|
||||
|
||||
## [0.15.14.0] - 2026-04-05
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`gstack-team-init` now detects and removes vendored gstack copies.** When you run `gstack-team-init` inside a repo that has gstack vendored at `.claude/skills/gstack/`, it automatically removes the vendored copy, untracks it from git, and adds it to `.gitignore`. No more stale vendored copies shadowing the global install.
|
||||
- **`/gstack-upgrade` respects team mode.** Step 4.5 now checks the `team_mode` config. In team mode, vendored copies are removed instead of synced, since the global install is the single source of truth.
|
||||
- **`team_mode` config key.** `./setup --team` and `./setup --no-team` now set a dedicated `team_mode` config key so the upgrade skill can reliably distinguish team mode from just having auto-upgrade enabled.
|
||||
|
||||
## [0.15.13.0] - 2026-04-04 — Team Mode
|
||||
|
||||
Teams can now keep every developer on the same gstack version automatically. No more vendoring 342 files into your repo. No more version drift across branches. No more "who upgraded gstack last?" Slack threads. One command, every developer is current.
|
||||
|
||||
@@ -29,6 +29,26 @@ REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
CLAUDE_MD="$REPO_ROOT/CLAUDE.md"
|
||||
GENERATED=()
|
||||
|
||||
# ── Migrate vendored copy if present ──────────────────────────
|
||||
|
||||
if [ -d "$REPO_ROOT/.claude/skills/gstack" ] && [ ! -L "$REPO_ROOT/.claude/skills/gstack" ]; then
|
||||
if [ -f "$REPO_ROOT/.claude/skills/gstack/VERSION" ] || [ -d "$REPO_ROOT/.claude/skills/gstack/.git" ]; then
|
||||
echo " Found vendored gstack copy at $REPO_ROOT/.claude/skills/gstack"
|
||||
echo " Team mode uses the global install — removing vendored copy..."
|
||||
( cd "$REPO_ROOT" && git rm -r --cached .claude/skills/gstack/ 2>/dev/null ) || true
|
||||
if [ -f "$REPO_ROOT/.gitignore" ]; then
|
||||
if ! grep -qF '.claude/skills/gstack/' "$REPO_ROOT/.gitignore" 2>/dev/null; then
|
||||
echo '.claude/skills/gstack/' >> "$REPO_ROOT/.gitignore"
|
||||
fi
|
||||
else
|
||||
echo '.claude/skills/gstack/' > "$REPO_ROOT/.gitignore"
|
||||
fi
|
||||
rm -rf "$REPO_ROOT/.claude/skills/gstack"
|
||||
GENERATED+=(".gitignore")
|
||||
echo " Removed vendored copy and added .claude/skills/gstack/ to .gitignore"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── CLAUDE.md snippet ──────────────────────────────────────────
|
||||
|
||||
if [ "$MODE" = "optional" ]; then
|
||||
|
||||
+21
-5
@@ -137,9 +137,9 @@ cd "$INSTALL_DIR" && ./setup
|
||||
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
|
||||
```
|
||||
|
||||
### Step 4.5: Sync local vendored copy
|
||||
### Step 4.5: Handle local vendored copy
|
||||
|
||||
Use the install directory from Step 2. Check if there's also a local vendored copy that needs updating:
|
||||
Use the install directory from Step 2. Check if there's also a local vendored copy, and whether team mode is active:
|
||||
|
||||
```bash
|
||||
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
@@ -151,10 +151,24 @@ if [ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ]; then
|
||||
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
|
||||
fi
|
||||
fi
|
||||
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
|
||||
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
|
||||
echo "TEAM_MODE=$_TEAM_MODE"
|
||||
```
|
||||
|
||||
If `LOCAL_GSTACK` is non-empty, update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy. Team mode uses the global install as the single source of truth.
|
||||
|
||||
```bash
|
||||
cd "$_ROOT"
|
||||
git rm -r --cached .claude/skills/gstack/ 2>/dev/null || true
|
||||
if ! grep -qF '.claude/skills/gstack/' .gitignore 2>/dev/null; then
|
||||
echo '.claude/skills/gstack/' >> .gitignore
|
||||
fi
|
||||
rm -rf "$LOCAL_GSTACK"
|
||||
```
|
||||
Tell user: "Removed vendored copy at `$LOCAL_GSTACK` (team mode active — global install is the source of truth). Commit the `.gitignore` change when ready."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:** Update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
|
||||
```bash
|
||||
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
|
||||
cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK"
|
||||
@@ -243,11 +257,13 @@ Use the output to determine if an upgrade is available.
|
||||
|
||||
3. If no output (primary is up to date): check for a stale local vendored copy.
|
||||
|
||||
Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`).
|
||||
Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`) and team mode status (`TEAM_MODE`).
|
||||
|
||||
**If `LOCAL_GSTACK` is empty** (no local vendored copy): tell the user "You're already on the latest version (v{version})."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty**, compare versions:
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy using the Step 4.5 team-mode removal bash block above. Tell user: "Global v{version} is up to date. Removed stale vendored copy (team mode active). Commit the `.gitignore` change when ready."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`**, compare versions:
|
||||
```bash
|
||||
PRIMARY_VER=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
||||
LOCAL_VER=$(cat "$LOCAL_GSTACK/VERSION" 2>/dev/null || echo "unknown")
|
||||
|
||||
@@ -139,9 +139,9 @@ cd "$INSTALL_DIR" && ./setup
|
||||
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
|
||||
```
|
||||
|
||||
### Step 4.5: Sync local vendored copy
|
||||
### Step 4.5: Handle local vendored copy
|
||||
|
||||
Use the install directory from Step 2. Check if there's also a local vendored copy that needs updating:
|
||||
Use the install directory from Step 2. Check if there's also a local vendored copy, and whether team mode is active:
|
||||
|
||||
```bash
|
||||
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
||||
@@ -153,10 +153,24 @@ if [ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ]; then
|
||||
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
|
||||
fi
|
||||
fi
|
||||
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
|
||||
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
|
||||
echo "TEAM_MODE=$_TEAM_MODE"
|
||||
```
|
||||
|
||||
If `LOCAL_GSTACK` is non-empty, update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy. Team mode uses the global install as the single source of truth.
|
||||
|
||||
```bash
|
||||
cd "$_ROOT"
|
||||
git rm -r --cached .claude/skills/gstack/ 2>/dev/null || true
|
||||
if ! grep -qF '.claude/skills/gstack/' .gitignore 2>/dev/null; then
|
||||
echo '.claude/skills/gstack/' >> .gitignore
|
||||
fi
|
||||
rm -rf "$LOCAL_GSTACK"
|
||||
```
|
||||
Tell user: "Removed vendored copy at `$LOCAL_GSTACK` (team mode active — global install is the source of truth). Commit the `.gitignore` change when ready."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:** Update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
|
||||
```bash
|
||||
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
|
||||
cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK"
|
||||
@@ -245,11 +259,13 @@ Use the output to determine if an upgrade is available.
|
||||
|
||||
3. If no output (primary is up to date): check for a stale local vendored copy.
|
||||
|
||||
Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`).
|
||||
Run the Step 2 bash block above to detect the primary install type and directory (`INSTALL_TYPE` and `INSTALL_DIR`). Then run the Step 4.5 detection bash block above to check for a local vendored copy (`LOCAL_GSTACK`) and team mode status (`TEAM_MODE`).
|
||||
|
||||
**If `LOCAL_GSTACK` is empty** (no local vendored copy): tell the user "You're already on the latest version (v{version})."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty**, compare versions:
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy using the Step 4.5 team-mode removal bash block above. Tell user: "Global v{version} is up to date. Removed stale vendored copy (team mode active). Commit the `.gitignore` change when ready."
|
||||
|
||||
**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`**, compare versions:
|
||||
```bash
|
||||
PRIMARY_VER=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
||||
LOCAL_VER=$(cat "$LOCAL_GSTACK/VERSION" 2>/dev/null || echo "unknown")
|
||||
|
||||
@@ -785,6 +785,7 @@ HOOK_CMD="$SOURCE_GSTACK_DIR/bin/gstack-session-update"
|
||||
|
||||
if [ "$TEAM_MODE" -eq 1 ]; then
|
||||
"$GSTACK_CONFIG" set auto_upgrade true 2>/dev/null || true
|
||||
"$GSTACK_CONFIG" set team_mode true 2>/dev/null || true
|
||||
|
||||
# Register SessionStart hook in Claude Code settings
|
||||
if [ -x "$SETTINGS_HOOK" ]; then
|
||||
@@ -802,6 +803,7 @@ fi
|
||||
|
||||
if [ "$NO_TEAM_MODE" -eq 1 ]; then
|
||||
"$GSTACK_CONFIG" set auto_upgrade false 2>/dev/null || true
|
||||
"$GSTACK_CONFIG" set team_mode false 2>/dev/null || true
|
||||
|
||||
# Remove SessionStart hook from Claude Code settings
|
||||
if [ -x "$SETTINGS_HOOK" ]; then
|
||||
|
||||
@@ -257,6 +257,69 @@ describe('gstack-team-init', () => {
|
||||
const matches = claude.match(/## gstack/g);
|
||||
expect(matches).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('removes vendored copy when present', () => {
|
||||
// Create a fake vendored gstack with VERSION file
|
||||
const vendoredDir = path.join(tmpDir, '.claude', 'skills', 'gstack');
|
||||
fs.mkdirSync(vendoredDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(vendoredDir, 'VERSION'), '0.14.0.0');
|
||||
fs.writeFileSync(path.join(vendoredDir, 'README.md'), 'vendored');
|
||||
// Track it in git
|
||||
execSync('git add .claude/skills/gstack/', { cwd: tmpDir });
|
||||
execSync('git commit -m "add vendored gstack"', { cwd: tmpDir });
|
||||
|
||||
const result = run(`${TEAM_INIT} optional`, { cwd: tmpDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain('Found vendored gstack copy');
|
||||
expect(result.stdout).toContain('Removed vendored copy');
|
||||
// Vendored dir should be gone
|
||||
expect(fs.existsSync(vendoredDir)).toBe(false);
|
||||
// .gitignore should have the entry
|
||||
const gitignore = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8');
|
||||
expect(gitignore).toContain('.claude/skills/gstack/');
|
||||
});
|
||||
|
||||
test('skips when no vendored copy present', () => {
|
||||
const result = run(`${TEAM_INIT} optional`, { cwd: tmpDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).not.toContain('Found vendored gstack copy');
|
||||
});
|
||||
|
||||
test('skips when .claude/skills/gstack is a symlink', () => {
|
||||
// Create a symlink (not a real vendored copy)
|
||||
const skillsDir = path.join(tmpDir, '.claude', 'skills');
|
||||
fs.mkdirSync(skillsDir, { recursive: true });
|
||||
const targetDir = mkTmpDir();
|
||||
fs.writeFileSync(path.join(targetDir, 'VERSION'), '0.14.0.0');
|
||||
fs.symlinkSync(targetDir, path.join(skillsDir, 'gstack'));
|
||||
|
||||
const result = run(`${TEAM_INIT} optional`, { cwd: tmpDir });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).not.toContain('Found vendored gstack copy');
|
||||
// Symlink should still exist
|
||||
expect(fs.lstatSync(path.join(skillsDir, 'gstack')).isSymbolicLink()).toBe(true);
|
||||
fs.rmSync(targetDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('does not duplicate .gitignore entry on re-run', () => {
|
||||
// Create vendored copy
|
||||
const vendoredDir = path.join(tmpDir, '.claude', 'skills', 'gstack');
|
||||
fs.mkdirSync(vendoredDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(vendoredDir, 'VERSION'), '0.14.0.0');
|
||||
execSync('git add .claude/skills/gstack/', { cwd: tmpDir });
|
||||
execSync('git commit -m "add vendored"', { cwd: tmpDir });
|
||||
|
||||
run(`${TEAM_INIT} optional`, { cwd: tmpDir });
|
||||
|
||||
// Re-create vendored dir to simulate re-run scenario
|
||||
fs.mkdirSync(vendoredDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(vendoredDir, 'VERSION'), '0.14.0.0');
|
||||
run(`${TEAM_INIT} optional`, { cwd: tmpDir });
|
||||
|
||||
const gitignore = fs.readFileSync(path.join(tmpDir, '.gitignore'), 'utf-8');
|
||||
const matches = gitignore.match(/\.claude\/skills\/gstack\//g);
|
||||
expect(matches).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setup --team / --no-team / -q', () => {
|
||||
|
||||
Reference in New Issue
Block a user