mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-18 18:32:28 +08: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"
|
CLAUDE_MD="$REPO_ROOT/CLAUDE.md"
|
||||||
GENERATED=()
|
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 ──────────────────────────────────────────
|
# ── CLAUDE.md snippet ──────────────────────────────────────────
|
||||||
|
|
||||||
if [ "$MODE" = "optional" ]; then
|
if [ "$MODE" = "optional" ]; then
|
||||||
|
|||||||
@@ -137,9 +137,9 @@ 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
|
### 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
|
```bash
|
||||||
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
_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"
|
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
|
||||||
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
|
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
|
```bash
|
||||||
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
|
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
|
||||||
cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK"
|
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.
|
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 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
|
```bash
|
||||||
PRIMARY_VER=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
PRIMARY_VER=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
||||||
LOCAL_VER=$(cat "$LOCAL_GSTACK/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"
|
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
|
```bash
|
||||||
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
|
_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"
|
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
|
||||||
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
|
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
|
```bash
|
||||||
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
|
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
|
||||||
cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK"
|
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.
|
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 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
|
```bash
|
||||||
PRIMARY_VER=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
PRIMARY_VER=$(cat "$INSTALL_DIR/VERSION" 2>/dev/null || echo "unknown")
|
||||||
LOCAL_VER=$(cat "$LOCAL_GSTACK/VERSION" 2>/dev/null || echo "unknown")
|
LOCAL_VER=$(cat "$LOCAL_GSTACK/VERSION" 2>/dev/null || echo "unknown")
|
||||||
|
|||||||
2
setup
2
setup
@@ -785,6 +785,7 @@ HOOK_CMD="$SOURCE_GSTACK_DIR/bin/gstack-session-update"
|
|||||||
|
|
||||||
if [ "$TEAM_MODE" -eq 1 ]; then
|
if [ "$TEAM_MODE" -eq 1 ]; then
|
||||||
"$GSTACK_CONFIG" set auto_upgrade true 2>/dev/null || true
|
"$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
|
# Register SessionStart hook in Claude Code settings
|
||||||
if [ -x "$SETTINGS_HOOK" ]; then
|
if [ -x "$SETTINGS_HOOK" ]; then
|
||||||
@@ -802,6 +803,7 @@ fi
|
|||||||
|
|
||||||
if [ "$NO_TEAM_MODE" -eq 1 ]; then
|
if [ "$NO_TEAM_MODE" -eq 1 ]; then
|
||||||
"$GSTACK_CONFIG" set auto_upgrade false 2>/dev/null || true
|
"$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
|
# Remove SessionStart hook from Claude Code settings
|
||||||
if [ -x "$SETTINGS_HOOK" ]; then
|
if [ -x "$SETTINGS_HOOK" ]; then
|
||||||
|
|||||||
@@ -257,6 +257,69 @@ describe('gstack-team-init', () => {
|
|||||||
const matches = claude.match(/## gstack/g);
|
const matches = claude.match(/## gstack/g);
|
||||||
expect(matches).toHaveLength(1);
|
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', () => {
|
describe('setup --team / --no-team / -q', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user