mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
feat: move browse state from /tmp to project-local .gstack/
CLI now uses config module for state paths, passes BROWSE_STATE_FILE to spawned server. Adds version mismatch auto-restart, legacy /tmp cleanup with PID verification, and removes stale global install fallback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+80
-13
@@ -2,22 +2,18 @@
|
||||
* gstack CLI — thin wrapper that talks to the persistent server
|
||||
*
|
||||
* Flow:
|
||||
* 1. Read /tmp/browse-server.json for port + token
|
||||
* 1. Read .gstack/browse.json for port + token
|
||||
* 2. If missing or stale PID → start server in background
|
||||
* 3. Health check
|
||||
* 3. Health check + version mismatch detection
|
||||
* 4. Send command via HTTP POST
|
||||
* 5. Print response to stdout (or stderr for errors)
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { resolveConfig, ensureStateDir, readVersionHash } from './config';
|
||||
|
||||
const PORT_OFFSET = 45600;
|
||||
const BROWSE_PORT = process.env.CONDUCTOR_PORT
|
||||
? parseInt(process.env.CONDUCTOR_PORT, 10) - PORT_OFFSET
|
||||
: 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`;
|
||||
const config = resolveConfig();
|
||||
const MAX_START_WAIT = 8000; // 8 seconds to start
|
||||
|
||||
export function resolveServerScript(
|
||||
@@ -45,8 +41,9 @@ export function resolveServerScript(
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy fallback for user-level installs
|
||||
return path.resolve(env.HOME || '/tmp', '.claude/skills/gstack/browse/src/server.ts');
|
||||
throw new Error(
|
||||
'Cannot find server.ts. Set BROWSE_SERVER_SCRIPT env or run from the browse source tree.'
|
||||
);
|
||||
}
|
||||
|
||||
const SERVER_SCRIPT = resolveServerScript();
|
||||
@@ -57,12 +54,13 @@ interface ServerState {
|
||||
token: string;
|
||||
startedAt: string;
|
||||
serverPath: string;
|
||||
binaryVersion?: string;
|
||||
}
|
||||
|
||||
// ─── State File ────────────────────────────────────────────────
|
||||
function readState(): ServerState | null {
|
||||
try {
|
||||
const data = fs.readFileSync(STATE_FILE, 'utf-8');
|
||||
const data = fs.readFileSync(config.stateFile, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
@@ -78,15 +76,73 @@ function isProcessAlive(pid: number): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Process Management ─────────────────────────────────────────
|
||||
async function killServer(pid: number): Promise<void> {
|
||||
if (!isProcessAlive(pid)) return;
|
||||
|
||||
try { process.kill(pid, 'SIGTERM'); } catch { return; }
|
||||
|
||||
// Wait up to 2s for graceful shutdown
|
||||
const deadline = Date.now() + 2000;
|
||||
while (Date.now() < deadline && isProcessAlive(pid)) {
|
||||
await Bun.sleep(100);
|
||||
}
|
||||
|
||||
// Force kill if still alive
|
||||
if (isProcessAlive(pid)) {
|
||||
try { process.kill(pid, 'SIGKILL'); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up legacy /tmp/browse-server*.json files from before project-local state.
|
||||
* Verifies PID ownership before sending signals.
|
||||
*/
|
||||
function cleanupLegacyState(): void {
|
||||
try {
|
||||
const files = fs.readdirSync('/tmp').filter(f => f.startsWith('browse-server') && f.endsWith('.json'));
|
||||
for (const file of files) {
|
||||
const fullPath = `/tmp/${file}`;
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
||||
if (data.pid && isProcessAlive(data.pid)) {
|
||||
// Verify this is actually a browse server before killing
|
||||
const check = Bun.spawnSync(['ps', '-p', String(data.pid), '-o', 'command='], {
|
||||
stdout: 'pipe', stderr: 'pipe', timeout: 2000,
|
||||
});
|
||||
const cmd = check.stdout.toString().trim();
|
||||
if (cmd.includes('bun') || cmd.includes('server.ts')) {
|
||||
try { process.kill(data.pid, 'SIGTERM'); } catch {}
|
||||
}
|
||||
}
|
||||
fs.unlinkSync(fullPath);
|
||||
} catch {
|
||||
// Best effort — skip files we can't parse or clean up
|
||||
}
|
||||
}
|
||||
// Clean up legacy log files too
|
||||
const logFiles = fs.readdirSync('/tmp').filter(f =>
|
||||
f.startsWith('browse-console') || f.startsWith('browse-network') || f.startsWith('browse-dialog')
|
||||
);
|
||||
for (const file of logFiles) {
|
||||
try { fs.unlinkSync(`/tmp/${file}`); } catch {}
|
||||
}
|
||||
} catch {
|
||||
// /tmp read failed — skip legacy cleanup
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Server Lifecycle ──────────────────────────────────────────
|
||||
async function startServer(): Promise<ServerState> {
|
||||
ensureStateDir(config);
|
||||
|
||||
// Clean up stale state file
|
||||
try { fs.unlinkSync(STATE_FILE); } catch {}
|
||||
try { fs.unlinkSync(config.stateFile); } catch {}
|
||||
|
||||
// Start server as detached background process
|
||||
const proc = Bun.spawn(['bun', 'run', SERVER_SCRIPT], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: { ...process.env },
|
||||
env: { ...process.env, BROWSE_STATE_FILE: config.stateFile },
|
||||
});
|
||||
|
||||
// Don't hold the CLI open
|
||||
@@ -120,6 +176,14 @@ async function ensureServer(): Promise<ServerState> {
|
||||
const state = readState();
|
||||
|
||||
if (state && isProcessAlive(state.pid)) {
|
||||
// Check for binary version mismatch (auto-restart on update)
|
||||
const currentVersion = readVersionHash();
|
||||
if (currentVersion && state.binaryVersion && currentVersion !== state.binaryVersion) {
|
||||
console.error('[browse] Binary updated, restarting server...');
|
||||
await killServer(state.pid);
|
||||
return startServer();
|
||||
}
|
||||
|
||||
// Server appears alive — do a health check
|
||||
try {
|
||||
const resp = await fetch(`http://127.0.0.1:${state.port}/health`, {
|
||||
@@ -237,6 +301,9 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// One-time cleanup of legacy /tmp state files
|
||||
cleanupLegacyState();
|
||||
|
||||
const command = args[0];
|
||||
const commandArgs = args.slice(1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user