diff --git a/CHANGELOG.md b/CHANGELOG.md index abec97d8..49a7caa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,20 @@ - SKILL.md rewritten as QA-oriented playbook with 10 workflow patterns - 166 integration tests (was ~63) +## 0.0.2 — 2026-03-12 + +- Fix project-local `/browse` installs — compiled binary now resolves `server.ts` from its own directory instead of assuming a global install exists +- `setup` rebuilds stale binaries (not just missing ones) and exits non-zero if the build fails +- Fix `chain` command swallowing real errors from write commands (e.g. navigation timeout reported as "Unknown meta command") +- Fix unbounded restart loop in CLI when server crashes repeatedly on the same command +- Cap console/network buffers at 50k entries (ring buffer) instead of growing without bound +- Fix disk flush stopping silently after buffer hits the 50k cap +- Fix `ln -snf` in setup to avoid creating nested symlinks on upgrade +- Use `git fetch && git reset --hard` instead of `git pull` for upgrades (handles force-pushes) +- Simplify install: global-first with optional project copy (replaces submodule approach) +- Restructured README: hero, before/after, demo transcript, troubleshooting section +- Six skills (added `/retro`) + ## 0.0.1 — 2026-03-11 Initial release. diff --git a/README.md b/README.md index 6f83815d..d9768177 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,23 @@ Everything lives inside `.claude/`. Nothing touches your PATH or runs in the bac --- +``` ++----------------------------------------------------------------------------+ +| | +| Are you a great software engineer who loves to write 10K LOC/day | +| and land 10 PRs a day like Garry? | +| | +| Come work at YC: ycombinator.com/software | +| | +| Extremely competitive salary and equity. | +| Now hiring in San Francisco, Dogpatch District. | +| Come join the revolution. | +| | ++----------------------------------------------------------------------------+ +``` + +--- + ## How I use these skills Created by [Garry Tan](https://x.com/garrytan), President & CEO of [Y Combinator](https://www.ycombinator.com/). diff --git a/browse/src/cli.ts b/browse/src/cli.ts index eb7a54bd..dae76fb9 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -18,13 +18,39 @@ const BROWSE_PORT = process.env.CONDUCTOR_PORT : parseInt(process.env.BROWSE_PORT || '0', 10); const INSTANCE_SUFFIX = BROWSE_PORT ? `-${BROWSE_PORT}` : ''; const STATE_FILE = process.env.BROWSE_STATE_FILE || `/tmp/browse-server${INSTANCE_SUFFIX}.json`; -// When compiled, import.meta.dir is virtual. Use env var or well-known path. -const SERVER_SCRIPT = process.env.BROWSE_SERVER_SCRIPT - || (import.meta.dir.startsWith('/') && !import.meta.dir.includes('$bunfs') - ? path.resolve(import.meta.dir, 'server.ts') - : path.resolve(process.env.HOME || '/tmp', '.claude/skills/gstack/browse/src/server.ts')); const MAX_START_WAIT = 8000; // 8 seconds to start +export function resolveServerScript( + env: Record = process.env, + metaDir: string = import.meta.dir, + execPath: string = process.execPath +): string { + if (env.BROWSE_SERVER_SCRIPT) { + return env.BROWSE_SERVER_SCRIPT; + } + + // Dev mode: cli.ts runs directly from browse/src + if (metaDir.startsWith('/') && !metaDir.includes('$bunfs')) { + const direct = path.resolve(metaDir, 'server.ts'); + if (fs.existsSync(direct)) { + return direct; + } + } + + // Compiled binary: derive the source tree from browse/dist/browse + if (execPath) { + const adjacent = path.resolve(path.dirname(execPath), '..', 'src', 'server.ts'); + if (fs.existsSync(adjacent)) { + return adjacent; + } + } + + // Legacy fallback for user-level installs + return path.resolve(env.HOME || '/tmp', '.claude/skills/gstack/browse/src/server.ts'); +} + +const SERVER_SCRIPT = resolveServerScript(); + interface ServerState { pid: number; port: number; @@ -224,7 +250,9 @@ Refs: After 'snapshot', use @e1, @e2... as selectors: await sendCommand(state, command, commandArgs); } -main().catch((err) => { - console.error(`[browse] ${err.message}`); - process.exit(1); -}); +if (import.meta.main) { + main().catch((err) => { + console.error(`[browse] ${err.message}`); + process.exit(1); + }); +} diff --git a/browse/test/commands.test.ts b/browse/test/commands.test.ts index 71ce3dc8..312b8cee 100644 --- a/browse/test/commands.test.ts +++ b/browse/test/commands.test.ts @@ -8,6 +8,7 @@ import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { startTestServer } from './test-server'; import { BrowserManager } from '../src/browser-manager'; +import { resolveServerScript } from '../src/cli'; import { handleReadCommand } from '../src/read-commands'; import { handleWriteCommand } from '../src/write-commands'; import { handleMetaCommand } from '../src/meta-commands'; @@ -420,34 +421,74 @@ describe('Status', () => { }); }); -// ─── CLI retry guard ──────────────────────────────────────────── +// ─── CLI server script resolution ─────────────────────────────── -describe('CLI retry guard', () => { - test('sendCommand aborts after repeated connection failures', async () => { - // Use an isolated state file to avoid conflicts with running servers - const stateFile = '/tmp/browse-server-test-retry.json'; - fs.writeFileSync(stateFile, JSON.stringify({ port: 1, token: 'fake', pid: 999999 })); +describe('CLI server script resolution', () => { + test('prefers adjacent browse/src/server.ts for compiled project installs', () => { + const root = fs.mkdtempSync('/tmp/gstack-cli-'); + const execPath = path.join(root, '.claude/skills/gstack/browse/dist/browse'); + const serverPath = path.join(root, '.claude/skills/gstack/browse/src/server.ts'); + + fs.mkdirSync(path.dirname(execPath), { recursive: true }); + fs.mkdirSync(path.dirname(serverPath), { recursive: true }); + fs.writeFileSync(serverPath, '// test server\n'); + + const resolved = resolveServerScript( + { HOME: path.join(root, 'empty-home') }, + '$bunfs/root', + execPath + ); + + expect(resolved).toBe(serverPath); + + fs.rmSync(root, { recursive: true, force: true }); + }); +}); + +// ─── CLI lifecycle ────────────────────────────────────────────── + +describe('CLI lifecycle', () => { + test('dead state file triggers a clean restart', async () => { + const stateFile = `/tmp/browse-test-state-${Date.now()}.json`; + fs.writeFileSync(stateFile, JSON.stringify({ + port: 1, + token: 'fake', + pid: 999999, + })); const cliPath = path.resolve(__dirname, '../src/cli.ts'); - const result = await new Promise<{ code: number; stderr: string }>((resolve) => { + // Build env without CONDUCTOR_PORT/BROWSE_PORT so BROWSE_PORT_START takes effect + const cliEnv: Record = {}; + for (const [k, v] of Object.entries(process.env)) { + if (k !== 'CONDUCTOR_PORT' && k !== 'BROWSE_PORT' && v !== undefined) cliEnv[k] = v; + } + cliEnv.BROWSE_STATE_FILE = stateFile; + // Use a random high port to avoid conflicts with running servers + cliEnv.BROWSE_PORT_START = String(9600 + Math.floor(Math.random() * 100)); + const result = await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => { const proc = spawn('bun', ['run', cliPath, 'status'], { timeout: 15000, - env: { - ...process.env, - BROWSE_STATE_FILE: stateFile, - BROWSE_PORT: '1', // Force port 1 (will fail) - }, + env: cliEnv, }); + let stdout = ''; let stderr = ''; + proc.stdout.on('data', (d) => stdout += d.toString()); proc.stderr.on('data', (d) => stderr += d.toString()); - proc.on('close', (code) => resolve({ code: code ?? 1, stderr })); + proc.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr })); }); - // Clean up - try { fs.unlinkSync(stateFile); } catch {} + let restartedPid: number | null = null; + if (fs.existsSync(stateFile)) { + restartedPid = JSON.parse(fs.readFileSync(stateFile, 'utf-8')).pid; + fs.unlinkSync(stateFile); + } + if (restartedPid) { + try { process.kill(restartedPid, 'SIGTERM'); } catch {} + } - // Should fail, not loop forever - expect(result.code).not.toBe(0); + expect(result.code).toBe(0); + expect(result.stdout).toContain('Status: healthy'); + expect(result.stderr).toContain('Starting server'); }, 20000); }); diff --git a/setup b/setup index b4b3a804..40430a42 100755 --- a/setup +++ b/setup @@ -13,14 +13,29 @@ ensure_playwright_browser() { ) >/dev/null 2>&1 } -# 1. Build browse binary if needed +# 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 "$GSTACK_DIR/browse/src" -type f -newer "$BROWSE_BIN" -print -quit 2>/dev/null)" ]; then + NEEDS_BUILD=1 +elif [ "$GSTACK_DIR/package.json" -nt "$BROWSE_BIN" ]; then + NEEDS_BUILD=1 +elif [ -f "$GSTACK_DIR/bun.lock" ] && [ "$GSTACK_DIR/bun.lock" -nt "$BROWSE_BIN" ]; then + NEEDS_BUILD=1 +fi + +if [ "$NEEDS_BUILD" -eq 1 ]; then echo "Building browse binary..." - cd "$GSTACK_DIR" && bun install && bun run build + ( + cd "$GSTACK_DIR" + bun install + bun run build + ) fi if [ ! -x "$BROWSE_BIN" ]; then - echo "gstack setup failed: browse binary not found at $BROWSE_BIN" >&2 + echo "gstack setup failed: browse binary missing at $BROWSE_BIN" >&2 exit 1 fi @@ -57,12 +72,12 @@ if [ "$SKILLS_BASENAME" = "skills" ]; then done echo "gstack ready." - echo " browse: $GSTACK_DIR/browse/dist/browse" + echo " browse: $BROWSE_BIN" if [ ${#linked[@]} -gt 0 ]; then echo " linked skills: ${linked[*]}" fi else echo "gstack ready." - echo " browse: $GSTACK_DIR/browse/dist/browse" + echo " browse: $BROWSE_BIN" echo " (skipped skill symlinks — not inside .claude/skills/)" fi