diff --git a/browse/src/cli.ts b/browse/src/cli.ts index d48fab9a9..c9a2ebd45 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -262,6 +262,11 @@ async function ensureServer(): Promise { } } + // The lockfile lives under config.stateDir. Ensure the directory exists + // before taking the lock so first run in a fresh repo doesn't misread + // ENOENT as "another instance is starting". + ensureStateDir(config); + // Acquire lock to prevent concurrent restart races (TOCTOU) const releaseLock = acquireServerLock(); if (!releaseLock) { diff --git a/browse/test/commands.test.ts b/browse/test/commands.test.ts index 8e6325673..ad82df679 100644 --- a/browse/test/commands.test.ts +++ b/browse/test/commands.test.ts @@ -686,6 +686,37 @@ describe('CLI server script resolution', () => { // ─── CLI lifecycle ────────────────────────────────────────────── describe('CLI lifecycle', () => { + test('missing default state dir triggers a clean first start', async () => { + const root = fs.mkdtempSync('/tmp/browse-first-start-'); + const cliPath = path.resolve(__dirname, '../src/cli.ts'); + const result = await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => { + const proc = spawn('bun', ['run', cliPath, 'status'], { + cwd: root, + timeout: 15000, + env: process.env, + }); + 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, stdout, stderr })); + }); + + let startedPid: number | null = null; + const stateFile = path.join(root, '.gstack', 'browse.json'); + if (fs.existsSync(stateFile)) { + startedPid = JSON.parse(fs.readFileSync(stateFile, 'utf-8')).pid; + } + if (startedPid) { + try { process.kill(startedPid, 'SIGTERM'); } catch {} + } + fs.rmSync(root, { recursive: true, force: true }); + + expect(result.code).toBe(0); + expect(result.stdout).toContain('Status: healthy'); + expect(result.stderr).toContain('Starting server'); + }, 20000); + test('dead state file triggers a clean restart', async () => { const stateFile = `/tmp/browse-test-state-${Date.now()}.json`; fs.writeFileSync(stateFile, JSON.stringify({