This commit is contained in:
Christopher Wong 2026-06-03 13:36:57 +08:00 committed by GitHub
commit e779b3fe1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 533 additions and 0 deletions

533
setup.ps1 Normal file
View File

@ -0,0 +1,533 @@
# gstack setup — build browser binary + register skills with Claude Code / Codex / Kiro
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
# ─── Administrative Check ──────────────────────────────────────
function Test-IsAdmin {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
if (-not (Test-IsAdmin)) {
Write-Warning "Administrator privileges are required to create symbolic links on Windows."
$response = Read-Host "Relaunch as Administrator? (Y/N)"
if ($response -eq 'Y') {
Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs
exit
} else {
Write-Host "[!] Aborted: Insufficient permissions." -ForegroundColor Red
exit 1
}
}
function Test-Command {
param($Name)
return [bool](Get-Command $Name -ErrorAction SilentlyContinue)
}
# ─── Dependency Checks ─────────────────────────────────────────
# 1. Bun
if (-not (Test-Command "bun")) {
Write-Host "`n[!] Error: Bun is required but not found." -ForegroundColor Red
Write-Host "Install: " -NoNewline; Write-Host "winget install -e --id Oven-sh.Bun`n" -ForegroundColor Cyan
exit 1
}
# 2. Node.js
if (-not (Test-Command "node")) {
Write-Host "`n[!] Error: Node.js is required for Playwright stability on Windows." -ForegroundColor Red
Write-Host "Install: " -NoNewline; Write-Host "winget install -e --id OpenJS.NodeJS`n" -ForegroundColor Cyan
exit 1
}
# 3. Git
if (-not (Test-Command "git")) {
Write-Host "`n[!] Error: Git is required to version the build." -ForegroundColor Red
Write-Host "Install: " -NoNewline; Write-Host "winget install -e --id Git.Git`n" -ForegroundColor Cyan
exit 1
}
# ─── Parse Flags ──────────────────────────────────────────────
$HOST_TARGET = "claude"
$LOCAL_INSTALL = $false
$i = 0
while ($i -lt $args.Count) {
switch ($args[$i]) {
"--host" {
if ($i + 1 -ge $args.Count) {
Write-Host "Missing value for --host (expected claude, codex, kiro, or auto)" -ForegroundColor Red
exit 1
}
$HOST_TARGET = $args[$i + 1]
$i += 2
}
{ $_ -match "^--host=" } {
$HOST_TARGET = $_ -replace "^--host=", ""
$i += 1
}
"--local" {
$LOCAL_INSTALL = $true
$i += 1
}
default { $i += 1 }
}
}
$validHosts = @("claude", "codex", "kiro", "auto")
if ($HOST_TARGET -notin $validHosts) {
Write-Host "Unknown --host value: $HOST_TARGET (expected claude, codex, kiro, or auto)" -ForegroundColor Red
exit 1
}
# ─── Setup Paths ───────────────────────────────────────────────
$SOURCE_GSTACK_DIR = (Get-Item $PSScriptRoot).FullName
$INSTALL_GSTACK_DIR = $PSScriptRoot
$INSTALL_SKILLS_DIR = Split-Path -Parent $INSTALL_GSTACK_DIR
$BROWSE_BIN = Join-Path $SOURCE_GSTACK_DIR "browse\dist\browse.exe"
$CODEX_SKILLS = Join-Path $HOME ".codex\skills"
$CODEX_GSTACK = Join-Path $CODEX_SKILLS "gstack"
# --local: install to .claude/skills/ in the current working directory
if ($LOCAL_INSTALL) {
if ($HOST_TARGET -eq "codex") {
Write-Host "Error: --local is only supported for Claude Code (not Codex)." -ForegroundColor Red
exit 1
}
$INSTALL_SKILLS_DIR = Join-Path (Get-Location).Path ".claude\skills"
if (-not (Test-Path $INSTALL_SKILLS_DIR)) {
New-Item -ItemType Directory -Path $INSTALL_SKILLS_DIR -Force | Out-Null
}
$HOST_TARGET = "claude"
}
# ─── Auto-detect installed agents ─────────────────────────────
$INSTALL_CLAUDE = $false
$INSTALL_CODEX = $false
$INSTALL_KIRO = $false
switch ($HOST_TARGET) {
"auto" {
if (Test-Command "claude") { $INSTALL_CLAUDE = $true }
if (Test-Command "codex") { $INSTALL_CODEX = $true }
if (Test-Command "kiro-cli") { $INSTALL_KIRO = $true }
# If none found, default to claude
if (-not $INSTALL_CLAUDE -and -not $INSTALL_CODEX -and -not $INSTALL_KIRO) {
$INSTALL_CLAUDE = $true
}
}
"claude" { $INSTALL_CLAUDE = $true }
"codex" { $INSTALL_CODEX = $true }
"kiro" { $INSTALL_KIRO = $true }
}
# ─── Helper Functions ──────────────────────────────────────────
function New-SymLink {
param(
[Parameter(Mandatory)] [string]$Source,
[Parameter(Mandatory)] [string]$Target
)
$absTargetDir = Split-Path -Parent $Target
if (-not (Test-Path $absTargetDir)) {
New-Item -ItemType Directory -Path $absTargetDir -Force | Out-Null
}
# Clean up existing before linking
if (Test-Path $Target) {
$item = Get-Item $Target -Force
if ($item.Attributes -match "ReparsePoint") {
Remove-Item $Target -Force
} elseif (Test-Path $Target -PathType Container) {
Remove-Item $Target -Recurse -Force
} else {
Remove-Item $Target -Force
}
}
New-Item -ItemType SymbolicLink -Path $Target -Value $Source -Force | Out-Null
}
function Link-ClaudeSkillDirs {
param(
[Parameter(Mandatory)] [string]$GstackDir,
[Parameter(Mandatory)] [string]$SkillsDir
)
$linked = @()
foreach ($dir in (Get-ChildItem -Path $GstackDir -Directory)) {
$skillMd = Join-Path $dir.FullName "SKILL.md"
if (Test-Path $skillMd) {
if ($dir.Name -eq "node_modules") { continue }
$target = Join-Path $SkillsDir $dir.Name
# Create or update symlink; skip if a real (non-symlink) file/directory exists
$existing = $null
if (Test-Path $target) { $existing = Get-Item $target -Force }
if ($null -eq $existing -or ($existing.Attributes -match "ReparsePoint")) {
New-SymLink -Source $dir.FullName -Target $target
$linked += $dir.Name
}
}
}
if ($linked.Count -gt 0) {
Write-Host " linked skills: $($linked -join ' ')"
}
}
function Link-CodexSkillDirs {
param(
[Parameter(Mandatory)] [string]$GstackDir,
[Parameter(Mandatory)] [string]$SkillsDir
)
$agentsDir = Join-Path $GstackDir ".agents\skills"
$linked = @()
if (-not (Test-Path $agentsDir)) {
Write-Host " Generating .agents/ skill docs..."
Push-Location $GstackDir
bun run gen:skill-docs --host codex
Pop-Location
}
if (-not (Test-Path $agentsDir)) {
Write-Warning ".agents/skills/ generation failed — run 'bun run gen:skill-docs --host codex' manually"
return
}
foreach ($dir in (Get-ChildItem -Path $agentsDir -Directory -Filter "gstack*")) {
$skillMd = Join-Path $dir.FullName "SKILL.md"
if (Test-Path $skillMd) {
# Skip the sidecar directory
if ($dir.Name -eq "gstack") { continue }
$target = Join-Path $SkillsDir $dir.Name
$existing = $null
if (Test-Path $target) { $existing = Get-Item $target -Force }
if ($null -eq $existing -or ($existing.Attributes -match "ReparsePoint")) {
New-SymLink -Source $dir.FullName -Target $target
$linked += $dir.Name
}
}
}
if ($linked.Count -gt 0) {
Write-Host " linked skills: $($linked -join ' ')"
}
}
function New-AgentsSidecar {
param(
[Parameter(Mandatory)] [string]$RepoRoot
)
$agentsGstack = Join-Path $RepoRoot ".agents\skills\gstack"
if (-not (Test-Path $agentsGstack)) {
New-Item -ItemType Directory -Path $agentsGstack -Force | Out-Null
}
# Sidecar directories that skills reference at runtime
foreach ($asset in @("bin", "browse", "review", "qa")) {
$src = Join-Path $SOURCE_GSTACK_DIR $asset
$dst = Join-Path $agentsGstack $asset
if (Test-Path $src) {
$existing = $null
if (Test-Path $dst) { $existing = Get-Item $dst -Force }
if ($null -eq $existing -or ($existing.Attributes -match "ReparsePoint")) {
New-SymLink -Source $src -Target $dst
}
}
}
# Sidecar files that skills reference at runtime
foreach ($file in @("ETHOS.md")) {
$src = Join-Path $SOURCE_GSTACK_DIR $file
$dst = Join-Path $agentsGstack $file
if (Test-Path $src) {
$existing = $null
if (Test-Path $dst) { $existing = Get-Item $dst -Force }
if ($null -eq $existing -or ($existing.Attributes -match "ReparsePoint")) {
New-SymLink -Source $src -Target $dst
}
}
}
}
function New-CodexRuntimeRoot {
param(
[Parameter(Mandatory)] [string]$GstackDir,
[Parameter(Mandatory)] [string]$CodexGstack
)
$agentsDir = Join-Path $GstackDir ".agents\skills"
# Clean up old installs
if (Test-Path $CodexGstack) {
$item = Get-Item $CodexGstack -Force
if ($item.Attributes -match "ReparsePoint") {
Remove-Item $CodexGstack -Force
} elseif ($CodexGstack -ne $GstackDir) {
Remove-Item $CodexGstack -Recurse -Force
}
}
New-Item -ItemType Directory -Path $CodexGstack -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $CodexGstack "browse") -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $CodexGstack "gstack-upgrade") -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $CodexGstack "review") -Force | Out-Null
# Root SKILL.md
$rootSkill = Join-Path $agentsDir "gstack\SKILL.md"
if (Test-Path $rootSkill) {
New-SymLink -Source $rootSkill -Target (Join-Path $CodexGstack "SKILL.md")
}
# bin/
$binDir = Join-Path $GstackDir "bin"
if (Test-Path $binDir) {
New-SymLink -Source $binDir -Target (Join-Path $CodexGstack "bin")
}
# browse/dist/
$browseDist = Join-Path $GstackDir "browse\dist"
if (Test-Path $browseDist) {
New-SymLink -Source $browseDist -Target (Join-Path $CodexGstack "browse\dist")
}
# browse/bin/
$browseBin = Join-Path $GstackDir "browse\bin"
if (Test-Path $browseBin) {
New-SymLink -Source $browseBin -Target (Join-Path $CodexGstack "browse\bin")
}
# gstack-upgrade SKILL.md
$upgradeSkill = Join-Path $agentsDir "gstack-upgrade\SKILL.md"
if (Test-Path $upgradeSkill) {
New-SymLink -Source $upgradeSkill -Target (Join-Path $CodexGstack "gstack-upgrade\SKILL.md")
}
# Review runtime assets (individual files, NOT the whole review/ dir)
foreach ($f in @("checklist.md", "design-checklist.md", "greptile-triage.md", "TODOS-format.md")) {
$src = Join-Path $GstackDir "review\$f"
if (Test-Path $src) {
New-SymLink -Source $src -Target (Join-Path $CodexGstack "review\$f")
}
}
# ETHOS.md
$ethos = Join-Path $GstackDir "ETHOS.md"
if (Test-Path $ethos) {
New-SymLink -Source $ethos -Target (Join-Path $CodexGstack "ETHOS.md")
}
}
function Ensure-PlaywrightBrowser {
# On Windows, Bun can't launch Chromium (oven-sh/bun#4253). Use Node.js.
Push-Location $SOURCE_GSTACK_DIR
try {
$result = node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); await b.close(); })()" 2>&1
$success = $LASTEXITCODE -eq 0
} catch {
$success = $false
}
Pop-Location
return $success
}
# ─── 1. Build Browse Binary ─────────────────────────────────────
$NEEDS_BUILD = $false
if (-not (Test-Path $BROWSE_BIN)) {
$NEEDS_BUILD = $true
} else {
$binTime = (Get-Item $BROWSE_BIN).LastWriteTime
$srcPath = Join-Path $SOURCE_GSTACK_DIR "browse\src"
if (Test-Path $srcPath) {
$latestSrc = Get-ChildItem -Path $srcPath -Recurse | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($latestSrc -and $latestSrc.LastWriteTime -gt $binTime) { $NEEDS_BUILD = $true }
}
$pkgJson = Join-Path $SOURCE_GSTACK_DIR "package.json"
if ((Test-Path $pkgJson) -and (Get-Item $pkgJson).LastWriteTime -gt $binTime) { $NEEDS_BUILD = $true }
$bunLock = Join-Path $SOURCE_GSTACK_DIR "bun.lock"
if ((Test-Path $bunLock) -and (Get-Item $bunLock).LastWriteTime -gt $binTime) { $NEEDS_BUILD = $true }
}
if ($NEEDS_BUILD) {
Write-Host "Building browse binary..." -ForegroundColor Gray
Push-Location $SOURCE_GSTACK_DIR
bun install
bun run build
Pop-Location
# Safety net: write .version if build script didn't
$versionFile = Join-Path $SOURCE_GSTACK_DIR "browse\dist\.version"
if (-not (Test-Path $versionFile)) {
git -C $SOURCE_GSTACK_DIR rev-parse HEAD | Out-File -FilePath $versionFile -Encoding utf8
}
}
if (-not (Test-Path $BROWSE_BIN)) {
Write-Host "gstack setup failed: browse binary missing at $BROWSE_BIN" -ForegroundColor Red
exit 1
}
# 1b. Generate .agents/ Codex skill docs — always regenerate to prevent stale descriptions
$AGENTS_DIR = Join-Path $SOURCE_GSTACK_DIR ".agents\skills"
if (-not $NEEDS_BUILD) {
Write-Host "Generating .agents/ skill docs..." -ForegroundColor Gray
Push-Location $SOURCE_GSTACK_DIR
try { bun install --frozen-lockfile 2>$null } catch { bun install }
bun run gen:skill-docs --host codex
Pop-Location
}
# ─── 2. Playwright Verification ─────────────────────────────────
if (-not (Ensure-PlaywrightBrowser)) {
Write-Host "Installing Playwright Chromium..." -ForegroundColor Gray
Push-Location $SOURCE_GSTACK_DIR
bunx playwright install chromium
# Verify Node can load Playwright
Write-Host "Windows detected — verifying Node.js can load Playwright..."
$nodeCheck = node -e "require('playwright')" 2>&1
if ($LASTEXITCODE -ne 0) {
npm install --no-save playwright
}
Pop-Location
}
if (-not (Ensure-PlaywrightBrowser)) {
Write-Host "gstack setup failed: Playwright Chromium could not be launched via Node.js" -ForegroundColor Red
Write-Host " This is a known issue with Bun on Windows (oven-sh/bun#4253)." -ForegroundColor Red
Write-Host " Ensure Node.js is installed and 'node -e `"require('playwright')`"' works." -ForegroundColor Red
exit 1
}
# ─── 3. Ensure ~/.gstack global state directory exists ──────────
$GSTACK_GLOBAL = Join-Path $HOME ".gstack\projects"
if (-not (Test-Path $GSTACK_GLOBAL)) {
New-Item -ItemType Directory -Path $GSTACK_GLOBAL -Force | Out-Null
}
# ─── Detect repo-local Codex install ────────────────────────────
$SKILLS_BASENAME = Split-Path $INSTALL_SKILLS_DIR -Leaf
$SKILLS_PARENT_BASENAME = Split-Path (Split-Path $INSTALL_SKILLS_DIR -Parent) -Leaf
$CODEX_REPO_LOCAL = ($SKILLS_BASENAME -eq "skills") -and ($SKILLS_PARENT_BASENAME -eq ".agents")
# ─── 4. Install for Claude ─────────────────────────────────────
if ($INSTALL_CLAUDE) {
if ($SKILLS_BASENAME -eq "skills") {
Link-ClaudeSkillDirs -GstackDir $SOURCE_GSTACK_DIR -SkillsDir $INSTALL_SKILLS_DIR
if ($LOCAL_INSTALL) {
Write-Host "gstack ready (project-local)." -ForegroundColor Green
Write-Host " skills: $INSTALL_SKILLS_DIR"
} else {
Write-Host "gstack ready (claude)." -ForegroundColor Green
}
Write-Host " browse: $BROWSE_BIN"
} else {
Write-Host "gstack ready (claude)." -ForegroundColor Green
Write-Host " browse: $BROWSE_BIN"
Write-Host " (skipped skill symlinks — not inside .claude/skills/)"
}
}
# ─── 5. Install for Codex ──────────────────────────────────────
if ($INSTALL_CODEX) {
if ($CODEX_REPO_LOCAL) {
$CODEX_SKILLS = $INSTALL_SKILLS_DIR
$CODEX_GSTACK = $INSTALL_GSTACK_DIR
}
if (-not (Test-Path $CODEX_SKILLS)) {
New-Item -ItemType Directory -Path $CODEX_SKILLS -Force | Out-Null
}
# Skip runtime root creation for repo-local installs
if (-not $CODEX_REPO_LOCAL) {
New-CodexRuntimeRoot -GstackDir $SOURCE_GSTACK_DIR -CodexGstack $CODEX_GSTACK
}
# Install generated Codex-format skills
Link-CodexSkillDirs -GstackDir $SOURCE_GSTACK_DIR -SkillsDir $CODEX_SKILLS
Write-Host "gstack ready (codex)." -ForegroundColor Green
Write-Host " browse: $BROWSE_BIN"
Write-Host " codex skills: $CODEX_SKILLS"
}
# ─── 6. Install for Kiro ───────────────────────────────────────
if ($INSTALL_KIRO) {
$KIRO_SKILLS = Join-Path $HOME ".kiro\skills"
$KIRO_GSTACK = Join-Path $KIRO_SKILLS "gstack"
if (-not (Test-Path $KIRO_SKILLS)) {
New-Item -ItemType Directory -Path $KIRO_SKILLS -Force | Out-Null
}
# Clean up old symlink
if ((Test-Path $KIRO_GSTACK) -and ((Get-Item $KIRO_GSTACK -Force).Attributes -match "ReparsePoint")) {
Remove-Item $KIRO_GSTACK -Force
}
New-Item -ItemType Directory -Path $KIRO_GSTACK -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $KIRO_GSTACK "browse") -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $KIRO_GSTACK "gstack-upgrade") -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $KIRO_GSTACK "review") -Force | Out-Null
# Runtime asset symlinks
New-SymLink -Source (Join-Path $SOURCE_GSTACK_DIR "bin") -Target (Join-Path $KIRO_GSTACK "bin")
$browseDist = Join-Path $SOURCE_GSTACK_DIR "browse\dist"
if (Test-Path $browseDist) {
New-SymLink -Source $browseDist -Target (Join-Path $KIRO_GSTACK "browse\dist")
}
$browseBinDir = Join-Path $SOURCE_GSTACK_DIR "browse\bin"
if (Test-Path $browseBinDir) {
New-SymLink -Source $browseBinDir -Target (Join-Path $KIRO_GSTACK "browse\bin")
}
# ETHOS.md
$ethos = Join-Path $SOURCE_GSTACK_DIR "ETHOS.md"
if (Test-Path $ethos) {
New-SymLink -Source $ethos -Target (Join-Path $KIRO_GSTACK "ETHOS.md")
}
# gstack-upgrade
$upgradeSkill = Join-Path $AGENTS_DIR "gstack-upgrade\SKILL.md"
if (Test-Path $upgradeSkill) {
New-SymLink -Source $upgradeSkill -Target (Join-Path $KIRO_GSTACK "gstack-upgrade\SKILL.md")
}
# Review runtime assets
foreach ($f in @("checklist.md", "design-checklist.md", "greptile-triage.md", "TODOS-format.md")) {
$src = Join-Path $SOURCE_GSTACK_DIR "review\$f"
if (Test-Path $src) {
New-SymLink -Source $src -Target (Join-Path $KIRO_GSTACK "review\$f")
}
}
# Rewrite root SKILL.md paths for Kiro
$skillMdContent = Get-Content (Join-Path $SOURCE_GSTACK_DIR "SKILL.md") -Raw
$skillMdContent = $skillMdContent -replace "~/.claude/skills/gstack", "~/.kiro/skills/gstack"
$skillMdContent = $skillMdContent -replace "\.claude/skills/gstack", ".kiro/skills/gstack"
$skillMdContent = $skillMdContent -replace "\.claude/skills", ".kiro/skills"
$skillMdContent | Out-File -FilePath (Join-Path $KIRO_GSTACK "SKILL.md") -Encoding utf8
# Link generated Codex-format skills with path rewriting
if (-not (Test-Path $AGENTS_DIR)) {
Write-Warning "no .agents/skills/ directory found — run 'bun run build' first"
} else {
foreach ($dir in (Get-ChildItem -Path $AGENTS_DIR -Directory -Filter "gstack*")) {
$skillMd = Join-Path $dir.FullName "SKILL.md"
if (-not (Test-Path $skillMd)) { continue }
$targetDir = Join-Path $KIRO_SKILLS $dir.Name
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
$content = Get-Content $skillMd -Raw
$content = $content -replace '\$HOME/.codex/skills/gstack', '$HOME/.kiro/skills/gstack'
$content = $content -replace "~/.codex/skills/gstack", "~/.kiro/skills/gstack"
$content = $content -replace "~/.claude/skills/gstack", "~/.kiro/skills/gstack"
$content | Out-File -FilePath (Join-Path $targetDir "SKILL.md") -Encoding utf8
}
Write-Host "gstack ready (kiro)." -ForegroundColor Green
Write-Host " browse: $BROWSE_BIN"
Write-Host " kiro skills: $KIRO_SKILLS"
}
}
# ─── 7. Create .agents/ sidecar symlinks ───────────────────────
if ($INSTALL_CODEX) {
New-AgentsSidecar -RepoRoot $SOURCE_GSTACK_DIR
}
# ─── 8. First-time welcome + cleanup ───────────────────────────
$gstackHome = Join-Path $HOME ".gstack"
if (-not (Test-Path $gstackHome)) {
New-Item -ItemType Directory -Path $gstackHome -Force | Out-Null
Write-Host " Welcome! Run /gstack-upgrade anytime to stay current."
}
$tmpVersion = Join-Path $env:TEMP "gstack-latest-version"
if (Test-Path $tmpVersion) { Remove-Item $tmpVersion }