~cytrogen/gstack

ref: c620de38e19fe63e8a6e361e11d706b5ed432b93 gstack/setup -rwxr-xr-x 27.8 KiB
c620de38 — Garry Tan fix: setup runs pending migrations so git pull + ./setup works (#774) 6 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
#!/usr/bin/env bash
# gstack setup — build browser binary + register skills with Claude Code / Codex
set -e

if ! command -v bun >/dev/null 2>&1; then
  echo "Error: bun is required but not installed." >&2
  echo "Install with checksum verification:" >&2
  echo '  BUN_VERSION="1.3.10"' >&2
  echo '  tmpfile=$(mktemp)' >&2
  echo '  curl -fsSL "https://bun.sh/install" -o "$tmpfile"' >&2
  echo '  echo "Verify checksum before running: shasum -a 256 $tmpfile"' >&2
  echo '  BUN_VERSION="$BUN_VERSION" bash "$tmpfile" && rm "$tmpfile"' >&2
  exit 1
fi

INSTALL_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd)"
SOURCE_GSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)"
INSTALL_SKILLS_DIR="$(dirname "$INSTALL_GSTACK_DIR")"
BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse"
CODEX_SKILLS="$HOME/.codex/skills"
CODEX_GSTACK="$CODEX_SKILLS/gstack"
FACTORY_SKILLS="$HOME/.factory/skills"
FACTORY_GSTACK="$FACTORY_SKILLS/gstack"

IS_WINDOWS=0
case "$(uname -s)" in
  MINGW*|MSYS*|CYGWIN*|Windows_NT) IS_WINDOWS=1 ;;
esac

# ─── Parse flags ──────────────────────────────────────────────
HOST="claude"
LOCAL_INSTALL=0
SKILL_PREFIX=1
SKILL_PREFIX_FLAG=0
while [ $# -gt 0 ]; do
  case "$1" in
    --host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
    --host=*) HOST="${1#--host=}"; shift ;;
    --local) LOCAL_INSTALL=1; shift ;;
    --prefix)    SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
    --no-prefix) SKILL_PREFIX=0; SKILL_PREFIX_FLAG=1; shift ;;
    *) shift ;;
  esac
done

case "$HOST" in
  claude|codex|kiro|factory|auto) ;;
  *) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, or auto)" >&2; exit 1 ;;
esac

# ─── Resolve skill prefix preference ─────────────────────────
# Priority: CLI flag > saved config > interactive prompt (or flat default for non-TTY)
GSTACK_CONFIG="$SOURCE_GSTACK_DIR/bin/gstack-config"
export GSTACK_SETUP_RUNNING=1  # Prevent gstack-config post-set hook from triggering relink mid-setup
if [ "$SKILL_PREFIX_FLAG" -eq 0 ]; then
  _saved_prefix="$("$GSTACK_CONFIG" get skill_prefix 2>/dev/null || true)"
  if [ "$_saved_prefix" = "true" ]; then
    SKILL_PREFIX=1
  elif [ "$_saved_prefix" = "false" ]; then
    SKILL_PREFIX=0
  else
    # No saved preference — prompt interactively (or default flat for non-TTY)
    if [ -t 0 ]; then
      echo ""
      echo "Skill naming: how should gstack skills appear?"
      echo ""
      echo "  1) Short names: /qa, /ship, /review"
      echo "     Recommended. Clean and fast to type."
      echo ""
      echo "  2) Namespaced: /gstack-qa, /gstack-ship, /gstack-review"
      echo "     Use this if you run other skill packs alongside gstack to avoid conflicts."
      echo ""
      printf "Choice [1/2] (default: 1, auto-selects in 10s): "
      read -t 10 -r _prefix_choice </dev/tty 2>/dev/null || _prefix_choice=""
      case "$_prefix_choice" in
        2) SKILL_PREFIX=1 ;;
        *) SKILL_PREFIX=0 ;;
      esac
    else
      SKILL_PREFIX=0
    fi
    # Save the choice for future runs
    "$GSTACK_CONFIG" set skill_prefix "$([ "$SKILL_PREFIX" -eq 1 ] && echo true || echo false)" 2>/dev/null || true
  fi
else
  # Flag was passed explicitly — persist the choice
  "$GSTACK_CONFIG" set skill_prefix "$([ "$SKILL_PREFIX" -eq 1 ] && echo true || echo false)" 2>/dev/null || true
fi

# --local: install to .claude/skills/ in the current working directory
if [ "$LOCAL_INSTALL" -eq 1 ]; then
  if [ "$HOST" = "codex" ]; then
    echo "Error: --local is only supported for Claude Code (not Codex)." >&2
    exit 1
  fi
  INSTALL_SKILLS_DIR="$(pwd)/.claude/skills"
  mkdir -p "$INSTALL_SKILLS_DIR"
  HOST="claude"
  INSTALL_CODEX=0
fi

# For auto: detect which agents are installed
INSTALL_CLAUDE=0
INSTALL_CODEX=0
INSTALL_KIRO=0
INSTALL_FACTORY=0
if [ "$HOST" = "auto" ]; then
  command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
  command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
  command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
  command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1
  # If none found, default to claude
  if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ]; then
    INSTALL_CLAUDE=1
  fi
elif [ "$HOST" = "claude" ]; then
  INSTALL_CLAUDE=1
elif [ "$HOST" = "codex" ]; then
  INSTALL_CODEX=1
elif [ "$HOST" = "kiro" ]; then
  INSTALL_KIRO=1
elif [ "$HOST" = "factory" ]; then
  INSTALL_FACTORY=1
fi

migrate_direct_codex_install() {
  local gstack_dir="$1"
  local codex_gstack="$2"
  local migrated_dir="$HOME/.gstack/repos/gstack"

  [ "$gstack_dir" = "$codex_gstack" ] || return 0
  [ -L "$gstack_dir" ] && return 0

  mkdir -p "$(dirname "$migrated_dir")"
  if [ -e "$migrated_dir" ] && [ "$migrated_dir" != "$gstack_dir" ]; then
    echo "gstack setup failed: direct Codex install detected at $gstack_dir" >&2
    echo "A migrated repo already exists at $migrated_dir; move one of them aside and rerun setup." >&2
    exit 1
  fi

  echo "Migrating direct Codex install to $migrated_dir to avoid duplicate skill discovery..."
  mv "$gstack_dir" "$migrated_dir"
  SOURCE_GSTACK_DIR="$migrated_dir"
  INSTALL_GSTACK_DIR="$migrated_dir"
  INSTALL_SKILLS_DIR="$(dirname "$INSTALL_GSTACK_DIR")"
  BROWSE_BIN="$SOURCE_GSTACK_DIR/browse/dist/browse"
}

if [ "$INSTALL_CODEX" -eq 1 ]; then
  migrate_direct_codex_install "$SOURCE_GSTACK_DIR" "$CODEX_GSTACK"
fi

ensure_playwright_browser() {
  if [ "$IS_WINDOWS" -eq 1 ]; then
    # On Windows, Bun can't launch Chromium due to broken pipe handling
    # (oven-sh/bun#4253). Use Node.js to verify Chromium works instead.
    (
      cd "$SOURCE_GSTACK_DIR"
      node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); await b.close(); })()" 2>/dev/null
    )
  else
    (
      cd "$SOURCE_GSTACK_DIR"
      bun --eval 'import { chromium } from "playwright"; const browser = await chromium.launch(); await browser.close();'
    ) >/dev/null 2>&1
  fi
}

# 1. Build browse binary if needed (smart rebuild: stale sources, package.json, lock)
NEEDS_BUILD=0
if [ ! -x "$BROWSE_BIN" ]; then
  NEEDS_BUILD=1
elif [ -n "$(find "$SOURCE_GSTACK_DIR/browse/src" -type f -newer "$BROWSE_BIN" -print -quit 2>/dev/null)" ]; then
  NEEDS_BUILD=1
elif [ "$SOURCE_GSTACK_DIR/package.json" -nt "$BROWSE_BIN" ]; then
  NEEDS_BUILD=1
elif [ -f "$SOURCE_GSTACK_DIR/bun.lock" ] && [ "$SOURCE_GSTACK_DIR/bun.lock" -nt "$BROWSE_BIN" ]; then
  NEEDS_BUILD=1
fi

if [ "$NEEDS_BUILD" -eq 1 ]; then
  echo "Building browse binary..."
  (
    cd "$SOURCE_GSTACK_DIR"
    bun install
    bun run build
  )
  # Safety net: write .version if build script didn't (e.g., git not available during build)
  if [ ! -f "$SOURCE_GSTACK_DIR/browse/dist/.version" ]; then
    git -C "$SOURCE_GSTACK_DIR" rev-parse HEAD > "$SOURCE_GSTACK_DIR/browse/dist/.version" 2>/dev/null || true
  fi
fi

if [ ! -x "$BROWSE_BIN" ]; then
  echo "gstack setup failed: browse binary missing at $BROWSE_BIN" >&2
  exit 1
fi

# 1b. Generate .agents/ Codex skill docs — always regenerate to prevent stale descriptions.
# .agents/ is no longer committed — generated at setup time from .tmpl templates.
# bun run build already does this, but we need it when NEEDS_BUILD=0 (binary is fresh).
# Always regenerate: generation is fast (<2s) and mtime-based staleness checks are fragile
# (miss stale files when timestamps match after clone/checkout/upgrade).
AGENTS_DIR="$SOURCE_GSTACK_DIR/.agents/skills"
NEEDS_AGENTS_GEN=1

if [ "$NEEDS_AGENTS_GEN" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
  echo "Generating .agents/ skill docs..."
  (
    cd "$SOURCE_GSTACK_DIR"
    bun install --frozen-lockfile 2>/dev/null || bun install
    bun run gen:skill-docs --host codex
  )
fi

# 1c. Generate .factory/ Factory Droid skill docs
if [ "$INSTALL_FACTORY" -eq 1 ] && [ "$NEEDS_BUILD" -eq 0 ]; then
  echo "Generating .factory/ skill docs..."
  (
    cd "$SOURCE_GSTACK_DIR"
    bun install --frozen-lockfile 2>/dev/null || bun install
    bun run gen:skill-docs --host factory
  )
fi

# 2. Ensure Playwright's Chromium is available
if ! ensure_playwright_browser; then
  echo "Installing Playwright Chromium..."
  (
    cd "$SOURCE_GSTACK_DIR"
    bunx playwright install chromium
  )

  if [ "$IS_WINDOWS" -eq 1 ]; then
    # On Windows, Node.js launches Chromium (not Bun — see oven-sh/bun#4253).
    # Ensure playwright is importable by Node from the gstack directory.
    if ! command -v node >/dev/null 2>&1; then
      echo "gstack setup failed: Node.js is required on Windows (Bun cannot launch Chromium due to a pipe bug)" >&2
      echo "  Install Node.js: https://nodejs.org/" >&2
      exit 1
    fi
    echo "Windows detected — verifying Node.js can load Playwright..."
    (
      cd "$SOURCE_GSTACK_DIR"
      # Bun's node_modules already has playwright; verify Node can require it
      node -e "require('playwright')" 2>/dev/null || npm install --no-save playwright
    )
  fi
fi

if ! ensure_playwright_browser; then
  if [ "$IS_WINDOWS" -eq 1 ]; then
    echo "gstack setup failed: Playwright Chromium could not be launched via Node.js" >&2
    echo "  This is a known issue with Bun on Windows (oven-sh/bun#4253)." >&2
    echo "  Ensure Node.js is installed and 'node -e \"require('playwright')\"' works." >&2
  else
    echo "gstack setup failed: Playwright Chromium could not be launched" >&2
  fi
  exit 1
fi

# 3. Ensure ~/.gstack global state directory exists
mkdir -p "$HOME/.gstack/projects"

# ─── Helper: link Claude skill subdirectories into a skills parent directory ──
# Creates real directories (not symlinks) at the top level with a SKILL.md symlink
# inside. This ensures Claude discovers them as top-level skills, not nested under
# gstack/ (which would auto-prefix them as gstack-*).
# When SKILL_PREFIX=1, directories are prefixed with "gstack-".
# Use --no-prefix to restore flat names.
link_claude_skill_dirs() {
  local gstack_dir="$1"
  local skills_dir="$2"
  local linked=()
  for skill_dir in "$gstack_dir"/*/; do
    if [ -f "$skill_dir/SKILL.md" ]; then
      dir_name="$(basename "$skill_dir")"
      # Skip node_modules
      [ "$dir_name" = "node_modules" ] && continue
      # Use frontmatter name: if present (e.g., run-tests/ with name: test → symlink as "test")
      skill_name=$(grep -m1 '^name:' "$skill_dir/SKILL.md" 2>/dev/null | sed 's/^name:[[:space:]]*//' | tr -d '[:space:]')
      [ -z "$skill_name" ] && skill_name="$dir_name"
      # Apply gstack- prefix unless --no-prefix or already prefixed
      if [ "$SKILL_PREFIX" -eq 1 ]; then
        case "$skill_name" in
          gstack-*) link_name="$skill_name" ;;
          *)        link_name="gstack-$skill_name" ;;
        esac
      else
        link_name="$skill_name"
      fi
      target="$skills_dir/$link_name"
      # Upgrade old directory symlinks to real directories
      if [ -L "$target" ]; then
        rm -f "$target"
      fi
      # Create real directory with symlinked SKILL.md (absolute path)
      if [ ! -e "$target" ] || [ -d "$target" ]; then
        mkdir -p "$target"
        ln -snf "$gstack_dir/$dir_name/SKILL.md" "$target/SKILL.md"
        linked+=("$link_name")
      fi
    fi
  done
  if [ ${#linked[@]} -gt 0 ]; then
    echo "  linked skills: ${linked[*]}"
  fi
}

# ─── Helper: remove old unprefixed Claude skill entries ───────────────────────
# Migration: when switching from flat names to gstack- prefixed names,
# clean up stale symlinks or directories that point into the gstack directory.
cleanup_old_claude_symlinks() {
  local gstack_dir="$1"
  local skills_dir="$2"
  local removed=()
  for skill_dir in "$gstack_dir"/*/; do
    if [ -f "$skill_dir/SKILL.md" ]; then
      skill_name="$(basename "$skill_dir")"
      [ "$skill_name" = "node_modules" ] && continue
      # Skip already-prefixed dirs (gstack-upgrade) — no old symlink to clean
      case "$skill_name" in gstack-*) continue ;; esac
      old_target="$skills_dir/$skill_name"
      # Remove directory symlinks pointing into gstack/
      if [ -L "$old_target" ]; then
        link_dest="$(readlink "$old_target" 2>/dev/null || true)"
        case "$link_dest" in
          gstack/*|*/gstack/*)
            rm -f "$old_target"
            removed+=("$skill_name")
            ;;
        esac
      # Remove real directories with symlinked SKILL.md pointing into gstack/
      elif [ -d "$old_target" ] && [ -L "$old_target/SKILL.md" ]; then
        link_dest="$(readlink "$old_target/SKILL.md" 2>/dev/null || true)"
        case "$link_dest" in
          *gstack*)
            rm -rf "$old_target"
            removed+=("$skill_name")
            ;;
        esac
      fi
    fi
  done
  if [ ${#removed[@]} -gt 0 ]; then
    echo "  cleaned up old entries: ${removed[*]}"
  fi
}

# ─── Helper: remove old prefixed Claude skill entries ─────────────────────────
# Reverse migration: when switching from gstack- prefixed names to flat names,
# clean up stale gstack-* symlinks or directories that point into the gstack directory.
cleanup_prefixed_claude_symlinks() {
  local gstack_dir="$1"
  local skills_dir="$2"
  local removed=()
  for skill_dir in "$gstack_dir"/*/; do
    if [ -f "$skill_dir/SKILL.md" ]; then
      skill_name="$(basename "$skill_dir")"
      [ "$skill_name" = "node_modules" ] && continue
      # Only clean up prefixed entries for dirs that AREN'T already prefixed
      # (e.g., remove gstack-qa but NOT gstack-upgrade which is the real dir name)
      case "$skill_name" in gstack-*) continue ;; esac
      prefixed_target="$skills_dir/gstack-$skill_name"
      # Remove directory symlinks pointing into gstack/
      if [ -L "$prefixed_target" ]; then
        link_dest="$(readlink "$prefixed_target" 2>/dev/null || true)"
        case "$link_dest" in
          gstack/*|*/gstack/*)
            rm -f "$prefixed_target"
            removed+=("gstack-$skill_name")
            ;;
        esac
      # Remove real directories with symlinked SKILL.md pointing into gstack/
      elif [ -d "$prefixed_target" ] && [ -L "$prefixed_target/SKILL.md" ]; then
        link_dest="$(readlink "$prefixed_target/SKILL.md" 2>/dev/null || true)"
        case "$link_dest" in
          *gstack*)
            rm -rf "$prefixed_target"
            removed+=("gstack-$skill_name")
            ;;
        esac
      fi
    fi
  done
  if [ ${#removed[@]} -gt 0 ]; then
    echo "  cleaned up prefixed entries: ${removed[*]}"
  fi
}

# ─── Helper: link generated Codex skills into a skills parent directory ──
# Installs from .agents/skills/gstack-* (the generated Codex-format skills)
# instead of source dirs (which have Claude paths).
link_codex_skill_dirs() {
  local gstack_dir="$1"
  local skills_dir="$2"
  local agents_dir="$gstack_dir/.agents/skills"
  local linked=()

  if [ ! -d "$agents_dir" ]; then
    echo "  Generating .agents/ skill docs..."
    ( cd "$gstack_dir" && bun run gen:skill-docs --host codex )
  fi

  if [ ! -d "$agents_dir" ]; then
    echo "  warning: .agents/skills/ generation failed — run 'bun run gen:skill-docs --host codex' manually" >&2
    return 1
  fi

  for skill_dir in "$agents_dir"/gstack*/; do
    if [ -f "$skill_dir/SKILL.md" ]; then
      skill_name="$(basename "$skill_dir")"
      # Skip the sidecar directory — it contains runtime asset symlinks (bin/,
      # browse/), not a skill. Linking it would overwrite the root gstack
      # symlink that Step 5 already pointed at the repo root.
      [ "$skill_name" = "gstack" ] && continue
      target="$skills_dir/$skill_name"
      # Create or update symlink
      if [ -L "$target" ] || [ ! -e "$target" ]; then
        ln -snf "$skill_dir" "$target"
        linked+=("$skill_name")
      fi
    fi
  done
  if [ ${#linked[@]} -gt 0 ]; then
    echo "  linked skills: ${linked[*]}"
  fi
}

# ─── Helper: create .agents/skills/gstack/ sidecar symlinks ──────────
# Codex/Gemini/Cursor read skills from .agents/skills/. We link runtime
# assets (bin/, browse/dist/, review/, qa/, etc.) so skill templates can
# resolve paths like $SKILL_ROOT/review/design-checklist.md.
create_agents_sidecar() {
  local repo_root="$1"
  local agents_gstack="$repo_root/.agents/skills/gstack"
  mkdir -p "$agents_gstack"

  # Sidecar directories that skills reference at runtime
  for asset in bin browse review qa; do
    local src="$SOURCE_GSTACK_DIR/$asset"
    local dst="$agents_gstack/$asset"
    if [ -d "$src" ] || [ -f "$src" ]; then
      if [ -L "$dst" ] || [ ! -e "$dst" ]; then
        ln -snf "$src" "$dst"
      fi
    fi
  done

  # Sidecar files that skills reference at runtime
  for file in ETHOS.md; do
    local src="$SOURCE_GSTACK_DIR/$file"
    local dst="$agents_gstack/$file"
    if [ -f "$src" ]; then
      if [ -L "$dst" ] || [ ! -e "$dst" ]; then
        ln -snf "$src" "$dst"
      fi
    fi
  done
}

# ─── Helper: create a minimal ~/.codex/skills/gstack runtime root ───────────
# Codex scans ~/.codex/skills recursively. Exposing the whole repo here causes
# duplicate skills because source SKILL.md files and generated Codex skills are
# both discoverable. Keep this directory limited to runtime assets + root skill.
create_codex_runtime_root() {
  local gstack_dir="$1"
  local codex_gstack="$2"
  local agents_dir="$gstack_dir/.agents/skills"

  if [ -L "$codex_gstack" ]; then
    rm -f "$codex_gstack"
  elif [ -d "$codex_gstack" ] && [ "$codex_gstack" != "$gstack_dir" ]; then
    # Old direct installs left a real directory here with stale source skills.
    # Remove it so we start fresh with only the minimal runtime assets.
    rm -rf "$codex_gstack"
  fi

  mkdir -p "$codex_gstack" "$codex_gstack/browse" "$codex_gstack/gstack-upgrade" "$codex_gstack/review"

  if [ -f "$agents_dir/gstack/SKILL.md" ]; then
    ln -snf "$agents_dir/gstack/SKILL.md" "$codex_gstack/SKILL.md"
  fi
  if [ -d "$gstack_dir/bin" ]; then
    ln -snf "$gstack_dir/bin" "$codex_gstack/bin"
  fi
  if [ -d "$gstack_dir/browse/dist" ]; then
    ln -snf "$gstack_dir/browse/dist" "$codex_gstack/browse/dist"
  fi
  if [ -d "$gstack_dir/browse/bin" ]; then
    ln -snf "$gstack_dir/browse/bin" "$codex_gstack/browse/bin"
  fi
  if [ -f "$agents_dir/gstack-upgrade/SKILL.md" ]; then
    ln -snf "$agents_dir/gstack-upgrade/SKILL.md" "$codex_gstack/gstack-upgrade/SKILL.md"
  fi
  # Review runtime assets (individual files, NOT the whole review/ dir which has SKILL.md)
  for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
    if [ -f "$gstack_dir/review/$f" ]; then
      ln -snf "$gstack_dir/review/$f" "$codex_gstack/review/$f"
    fi
  done
  # ETHOS.md — referenced by "Search Before Building" in all skill preambles
  if [ -f "$gstack_dir/ETHOS.md" ]; then
    ln -snf "$gstack_dir/ETHOS.md" "$codex_gstack/ETHOS.md"
  fi
}

create_factory_runtime_root() {
  local gstack_dir="$1"
  local factory_gstack="$2"
  local factory_dir="$gstack_dir/.factory/skills"

  if [ -L "$factory_gstack" ]; then
    rm -f "$factory_gstack"
  elif [ -d "$factory_gstack" ] && [ "$factory_gstack" != "$gstack_dir" ]; then
    rm -rf "$factory_gstack"
  fi

  mkdir -p "$factory_gstack" "$factory_gstack/browse" "$factory_gstack/gstack-upgrade" "$factory_gstack/review"

  if [ -f "$factory_dir/gstack/SKILL.md" ]; then
    ln -snf "$factory_dir/gstack/SKILL.md" "$factory_gstack/SKILL.md"
  fi
  if [ -d "$gstack_dir/bin" ]; then
    ln -snf "$gstack_dir/bin" "$factory_gstack/bin"
  fi
  if [ -d "$gstack_dir/browse/dist" ]; then
    ln -snf "$gstack_dir/browse/dist" "$factory_gstack/browse/dist"
  fi
  if [ -d "$gstack_dir/browse/bin" ]; then
    ln -snf "$gstack_dir/browse/bin" "$factory_gstack/browse/bin"
  fi
  if [ -f "$factory_dir/gstack-upgrade/SKILL.md" ]; then
    ln -snf "$factory_dir/gstack-upgrade/SKILL.md" "$factory_gstack/gstack-upgrade/SKILL.md"
  fi
  for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
    if [ -f "$gstack_dir/review/$f" ]; then
      ln -snf "$gstack_dir/review/$f" "$factory_gstack/review/$f"
    fi
  done
  if [ -f "$gstack_dir/ETHOS.md" ]; then
    ln -snf "$gstack_dir/ETHOS.md" "$factory_gstack/ETHOS.md"
  fi
}

link_factory_skill_dirs() {
  local gstack_dir="$1"
  local skills_dir="$2"
  local factory_dir="$gstack_dir/.factory/skills"
  local linked=()

  if [ ! -d "$factory_dir" ]; then
    echo "  Generating .factory/ skill docs..."
    ( cd "$gstack_dir" && bun run gen:skill-docs --host factory )
  fi

  if [ ! -d "$factory_dir" ]; then
    echo "  warning: .factory/skills/ generation failed — run 'bun run gen:skill-docs --host factory' manually" >&2
    return 1
  fi

  for skill_dir in "$factory_dir"/gstack*/; do
    if [ -f "$skill_dir/SKILL.md" ]; then
      skill_name="$(basename "$skill_dir")"
      [ "$skill_name" = "gstack" ] && continue
      target="$skills_dir/$skill_name"
      if [ -L "$target" ] || [ ! -e "$target" ]; then
        ln -snf "$skill_dir" "$target"
        linked+=("$skill_name")
      fi
    fi
  done
  if [ ${#linked[@]} -gt 0 ]; then
    echo "  linked skills: ${linked[*]}"
  fi
}

# 4. Install for Claude (default)
SKILLS_BASENAME="$(basename "$INSTALL_SKILLS_DIR")"
SKILLS_PARENT_BASENAME="$(basename "$(dirname "$INSTALL_SKILLS_DIR")")"
CODEX_REPO_LOCAL=0
if [ "$SKILLS_BASENAME" = "skills" ] && [ "$SKILLS_PARENT_BASENAME" = ".agents" ]; then
  CODEX_REPO_LOCAL=1
fi

if [ "$INSTALL_CLAUDE" -eq 1 ]; then
  if [ "$SKILLS_BASENAME" = "skills" ]; then
    # Clean up stale symlinks from the opposite prefix mode
    if [ "$SKILL_PREFIX" -eq 1 ]; then
      cleanup_old_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
    else
      cleanup_prefixed_claude_symlinks "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
    fi
    # Patch name: fields BEFORE creating symlinks so link_claude_skill_dirs
    # reads the correct (patched) name: values for symlink naming
    "$SOURCE_GSTACK_DIR/bin/gstack-patch-names" "$SOURCE_GSTACK_DIR" "$SKILL_PREFIX"
    link_claude_skill_dirs "$SOURCE_GSTACK_DIR" "$INSTALL_SKILLS_DIR"
    if [ "$LOCAL_INSTALL" -eq 1 ]; then
      echo "gstack ready (project-local)."
      echo "  skills: $INSTALL_SKILLS_DIR"
    else
      echo "gstack ready (claude)."
    fi
    echo "  browse: $BROWSE_BIN"
  else
    echo "gstack ready (claude)."
    echo "  browse: $BROWSE_BIN"
    echo "  (skipped skill symlinks — not inside .claude/skills/)"
  fi
fi

# 5. Install for Codex
if [ "$INSTALL_CODEX" -eq 1 ]; then
  if [ "$CODEX_REPO_LOCAL" -eq 1 ]; then
    CODEX_SKILLS="$INSTALL_SKILLS_DIR"
    CODEX_GSTACK="$INSTALL_GSTACK_DIR"
  fi
  mkdir -p "$CODEX_SKILLS"

  # Skip runtime root creation for repo-local installs — the checkout IS the runtime root.
  # create_codex_runtime_root would create self-referential symlinks (bin → bin, etc.).
  if [ "$CODEX_REPO_LOCAL" -eq 0 ]; then
    create_codex_runtime_root "$SOURCE_GSTACK_DIR" "$CODEX_GSTACK"
  fi
  # Install generated Codex-format skills (not Claude source dirs)
  link_codex_skill_dirs "$SOURCE_GSTACK_DIR" "$CODEX_SKILLS"

  echo "gstack ready (codex)."
  echo "  browse: $BROWSE_BIN"
  echo "  codex skills: $CODEX_SKILLS"
fi

# 6. Install for Kiro CLI (copy from .agents/skills, rewrite paths)
if [ "$INSTALL_KIRO" -eq 1 ]; then
  KIRO_SKILLS="$HOME/.kiro/skills"
  AGENTS_DIR="$SOURCE_GSTACK_DIR/.agents/skills"
  mkdir -p "$KIRO_SKILLS"

  # Create gstack dir with symlinks for runtime assets, copy+sed for SKILL.md
  KIRO_GSTACK="$KIRO_SKILLS/gstack"
  # Remove old whole-dir symlink from previous installs
  [ -L "$KIRO_GSTACK" ] && rm -f "$KIRO_GSTACK"
  mkdir -p "$KIRO_GSTACK" "$KIRO_GSTACK/browse" "$KIRO_GSTACK/gstack-upgrade" "$KIRO_GSTACK/review"
  ln -snf "$SOURCE_GSTACK_DIR/bin" "$KIRO_GSTACK/bin"
  ln -snf "$SOURCE_GSTACK_DIR/browse/dist" "$KIRO_GSTACK/browse/dist"
  ln -snf "$SOURCE_GSTACK_DIR/browse/bin" "$KIRO_GSTACK/browse/bin"
  # ETHOS.md — referenced by "Search Before Building" in all skill preambles
  if [ -f "$SOURCE_GSTACK_DIR/ETHOS.md" ]; then
    ln -snf "$SOURCE_GSTACK_DIR/ETHOS.md" "$KIRO_GSTACK/ETHOS.md"
  fi
  # gstack-upgrade skill
  if [ -f "$AGENTS_DIR/gstack-upgrade/SKILL.md" ]; then
    ln -snf "$AGENTS_DIR/gstack-upgrade/SKILL.md" "$KIRO_GSTACK/gstack-upgrade/SKILL.md"
  fi
  # Review runtime assets (individual files, not whole dir)
  for f in checklist.md design-checklist.md greptile-triage.md TODOS-format.md; do
    if [ -f "$SOURCE_GSTACK_DIR/review/$f" ]; then
      ln -snf "$SOURCE_GSTACK_DIR/review/$f" "$KIRO_GSTACK/review/$f"
    fi
  done

  # Rewrite root SKILL.md paths for Kiro
  sed -e "s|~/.claude/skills/gstack|~/.kiro/skills/gstack|g" \
      -e "s|\.claude/skills/gstack|.kiro/skills/gstack|g" \
      -e "s|\.claude/skills|.kiro/skills|g" \
      "$SOURCE_GSTACK_DIR/SKILL.md" > "$KIRO_GSTACK/SKILL.md"

  if [ ! -d "$AGENTS_DIR" ]; then
    echo "  warning: no .agents/skills/ directory found — run 'bun run build' first" >&2
  else
    for skill_dir in "$AGENTS_DIR"/gstack*/; do
      [ -f "$skill_dir/SKILL.md" ] || continue
      skill_name="$(basename "$skill_dir")"
      target_dir="$KIRO_SKILLS/$skill_name"
      mkdir -p "$target_dir"
      # Generated Codex skills use $HOME/.codex (not ~/), plus $GSTACK_ROOT variables.
      # Rewrite the default GSTACK_ROOT value and any remaining literal paths.
      sed -e 's|\$HOME/.codex/skills/gstack|$HOME/.kiro/skills/gstack|g' \
          -e "s|~/.codex/skills/gstack|~/.kiro/skills/gstack|g" \
          -e "s|~/.claude/skills/gstack|~/.kiro/skills/gstack|g" \
          "$skill_dir/SKILL.md" > "$target_dir/SKILL.md"
    done
    echo "gstack ready (kiro)."
    echo "  browse: $BROWSE_BIN"
    echo "  kiro skills: $KIRO_SKILLS"
  fi
fi

# 6b. Install for Factory Droid
if [ "$INSTALL_FACTORY" -eq 1 ]; then
  mkdir -p "$FACTORY_SKILLS"
  create_factory_runtime_root "$SOURCE_GSTACK_DIR" "$FACTORY_GSTACK"
  link_factory_skill_dirs "$SOURCE_GSTACK_DIR" "$FACTORY_SKILLS"
  echo "gstack ready (factory)."
  echo "  browse: $BROWSE_BIN"
  echo "  factory skills: $FACTORY_SKILLS"
fi

# 7. Create .agents/ sidecar symlinks for the real Codex skill target.
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
# so the runtime assets must live there for both global and repo-local installs.
if [ "$INSTALL_CODEX" -eq 1 ]; then
  create_agents_sidecar "$SOURCE_GSTACK_DIR"
fi

# 8. Run pending version migrations
# Migrations handle state fixes that ./setup alone can't cover (stale config,
# orphaned files, directory structure changes). Each migration is idempotent.
MIGRATIONS_DIR="$SOURCE_GSTACK_DIR/gstack-upgrade/migrations"
CURRENT_VERSION=$(cat "$SOURCE_GSTACK_DIR/VERSION" 2>/dev/null || echo "unknown")
LAST_SETUP_VERSION=$(cat "$HOME/.gstack/.last-setup-version" 2>/dev/null || echo "0.0.0.0")
if [ -d "$MIGRATIONS_DIR" ] && [ "$CURRENT_VERSION" != "unknown" ] && [ "$LAST_SETUP_VERSION" != "$CURRENT_VERSION" ]; then
  # Fresh install (no marker file) — skip migrations, just write marker
  if [ ! -f "$HOME/.gstack/.last-setup-version" ]; then
    : # fall through to marker write below
  else
    find "$MIGRATIONS_DIR" -maxdepth 1 -name 'v*.sh' -type f 2>/dev/null | sort -V | while IFS= read -r migration; do
      m_ver="$(basename "$migration" .sh | sed 's/^v//')"
      # Run if migration is newer than last setup version AND not newer than current version
      if [ "$(printf '%s\n%s' "$LAST_SETUP_VERSION" "$m_ver" | sort -V | head -1)" = "$LAST_SETUP_VERSION" ] && [ "$LAST_SETUP_VERSION" != "$m_ver" ] \
         && [ "$(printf '%s\n%s' "$m_ver" "$CURRENT_VERSION" | sort -V | tail -1)" = "$CURRENT_VERSION" ]; then
        echo "  running migration $m_ver..."
        bash "$migration" || echo "  warning: migration $m_ver had errors (non-fatal)"
      fi
    done
  fi
fi
mkdir -p "$HOME/.gstack"
if [ "$CURRENT_VERSION" != "unknown" ]; then
  echo "$CURRENT_VERSION" > "$HOME/.gstack/.last-setup-version"
fi

# 9. First-time welcome + legacy cleanup
if [ ! -f "$HOME/.gstack/.welcome-seen" ]; then
  echo "  Welcome! Run /gstack-upgrade anytime to stay current."
  touch "$HOME/.gstack/.welcome-seen"
fi
rm -f /tmp/gstack-latest-version