mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
fix: gstack-team-init detects and removes vendored copies in team mode
When running gstack-team-init inside a repo with a vendored .claude/skills/gstack/, the script now auto-detects and removes it: git rm --cached, add to .gitignore, rm -rf. Also adds team_mode config key to setup --team/--no-team, and makes gstack-upgrade Step 4.5 team-mode aware (remove instead of sync). Includes 5 new integration tests for the vendored copy migration.
This commit is contained in:
@@ -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