mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
fix(browse): Windows launcher extraEnv + headed-mode token (#822)
Community PR #822 by @pieterklue. Three fixes: 1. Windows launcher now merges extraEnv into spawned server env (was only passing BROWSE_STATE_FILE, dropping all other env vars) 2. Welcome page fallback serves inline HTML instead of about:blank redirect (avoids ERR_UNSAFE_REDIRECT on Windows) 3. /health returns auth token in headed mode even without Origin header (fixes Playwright Chromium extensions that don't send it) Also adds HOME/USERPROFILE fallback for cross-platform compatibility. Co-Authored-By: pieterklue <pieterklue@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -232,11 +232,12 @@ async function startServer(extraEnv?: Record<string, string>): Promise<ServerSta
|
||||
// when the CLI exits, the server dies with it. Use Node's child_process.spawn
|
||||
// with { detached: true } instead, which is the gold standard for Windows
|
||||
// process independence. Credit: PR #191 by @fqueiro.
|
||||
const extraEnvStr = JSON.stringify({ BROWSE_STATE_FILE: config.stateFile, ...(extraEnv || {}) });
|
||||
const launcherCode =
|
||||
`const{spawn}=require('child_process');` +
|
||||
`spawn(process.execPath,[${JSON.stringify(NODE_SERVER_SCRIPT)}],` +
|
||||
`{detached:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
|
||||
`{BROWSE_STATE_FILE:${JSON.stringify(config.stateFile)}})}).unref()`;
|
||||
`${extraEnvStr})}).unref()`;
|
||||
Bun.spawnSync(['node', '-e', launcherCode], { stdio: ['ignore', 'ignore', 'ignore'] });
|
||||
} else {
|
||||
// macOS/Linux: Bun.spawn + unref works correctly
|
||||
|
||||
+17
-7
@@ -1060,12 +1060,13 @@ async function start() {
|
||||
const welcomePath = (() => {
|
||||
// Check project-local designs first, then global
|
||||
const slug = process.env.GSTACK_SLUG || 'unknown';
|
||||
const projectWelcome = `${process.env.HOME}/.gstack/projects/${slug}/designs/welcome-page-20260331/finalized.html`;
|
||||
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
|
||||
const projectWelcome = `${homeDir}/.gstack/projects/${slug}/designs/welcome-page-20260331/finalized.html`;
|
||||
try { if (require('fs').existsSync(projectWelcome)) return projectWelcome; } catch (err: any) {
|
||||
console.warn('[browse] Error checking project welcome page:', err.message);
|
||||
}
|
||||
// Fallback: built-in welcome page from gstack install
|
||||
const skillRoot = process.env.GSTACK_SKILL_ROOT || `${process.env.HOME}/.claude/skills/gstack`;
|
||||
const skillRoot = process.env.GSTACK_SKILL_ROOT || `${homeDir}/.claude/skills/gstack`;
|
||||
const builtinWelcome = `${skillRoot}/browse/src/welcome.html`;
|
||||
try { if (require('fs').existsSync(builtinWelcome)) return builtinWelcome; } catch (err: any) {
|
||||
console.warn('[browse] Error checking builtin welcome page:', err.message);
|
||||
@@ -1080,8 +1081,14 @@ async function start() {
|
||||
console.error('[browse] Failed to read welcome page:', welcomePath, err.message);
|
||||
}
|
||||
}
|
||||
// No welcome page found — redirect to about:blank
|
||||
return new Response('', { status: 302, headers: { 'Location': 'about:blank' } });
|
||||
// No welcome page found — serve a simple fallback (avoid ERR_UNSAFE_REDIRECT on Windows)
|
||||
return new Response(
|
||||
`<!DOCTYPE html><html><head><title>GStack Browser</title>
|
||||
<style>body{background:#111;color:#fff;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;}
|
||||
.msg{text-align:center;opacity:.7;}.gold{color:#f5a623;font-size:2em;margin-bottom:12px;}</style></head>
|
||||
<body><div class="msg"><div class="gold">◈</div><p>GStack Browser ready.</p><p style="font-size:.85em">Waiting for commands from Claude Code.</p></div></body></html>`,
|
||||
{ status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Health check — no auth required, does NOT reset idle timer
|
||||
@@ -1093,11 +1100,14 @@ async function start() {
|
||||
uptime: Math.floor((Date.now() - startTime) / 1000),
|
||||
tabs: browserManager.getTabCount(),
|
||||
currentUrl: browserManager.getCurrentUrl(),
|
||||
// Auth token for extension bootstrap. Only returned when the request
|
||||
// comes from a Chrome extension (Origin: chrome-extension://...).
|
||||
// Auth token for extension bootstrap. Safe: /health is localhost-only.
|
||||
// Previously served unconditionally, but that leaks the token if the
|
||||
// server is tunneled to the internet (ngrok, SSH tunnel).
|
||||
...(req.headers.get('origin')?.startsWith('chrome-extension://') ? { token: AUTH_TOKEN } : {}),
|
||||
// In headed mode the server is always local, so return token unconditionally
|
||||
// (fixes Playwright Chromium extensions that don't send Origin header).
|
||||
...(browserManager.getConnectionMode() === 'headed' ||
|
||||
req.headers.get('origin')?.startsWith('chrome-extension://')
|
||||
? { token: AUTH_TOKEN } : {}),
|
||||
chatEnabled: true,
|
||||
agent: {
|
||||
status: agentStatus,
|
||||
|
||||
Reference in New Issue
Block a user