Skip to content

Commit 823903a

Browse files
committed
demo: merge garrytan#1120 homedir fix into all-applied baseline
# Conflicts: # browse/src/browser-manager.ts # browse/src/server.ts
2 parents 94a2a20 + d3a6b5e commit 823903a

7 files changed

Lines changed: 172 additions & 32 deletions

File tree

browse/src/browser-manager.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
* restores state. Falls back to clean slate on any failure.
1616
*/
1717

18+
import * as os from 'os';
1819
import { chromium, type Browser, type BrowserContext, type BrowserContextOptions, type Page, type Locator, type Cookie } from 'playwright';
1920
import { writeSecureFile, mkdirSecure } from './file-permissions';
2021
import { addConsoleEntry, addNetworkEntry, addDialogEntry, networkBuffer, type DialogEntry } from './buffers';
@@ -140,7 +141,7 @@ export class BrowserManager {
140141
// Relative to this source file (dev mode: browse/src/ -> ../../extension)
141142
path.resolve(__dirname, '..', '..', 'extension'),
142143
// Global gstack install
143-
path.join(process.env.HOME || '', '.claude', 'skills', 'gstack', 'extension'),
144+
path.join(os.homedir(), '.claude', 'skills', 'gstack', 'extension'),
144145
// Git repo root (detected via BROWSE_STATE_FILE location)
145146
(() => {
146147
const stateFile = process.env.BROWSE_STATE_FILE || '';
@@ -267,7 +268,7 @@ export class BrowserManager {
267268
if (authToken) {
268269
const fs = require('fs');
269270
const path = require('path');
270-
const gstackDir = path.join(process.env.HOME || '/tmp', '.gstack');
271+
const gstackDir = path.join(os.homedir(), '.gstack');
271272
mkdirSecure(gstackDir);
272273
const authFile = path.join(gstackDir, '.auth.json');
273274
try {
@@ -284,7 +285,7 @@ export class BrowserManager {
284285
// so we use Playwright's bundled Chromium which reliably loads extensions.
285286
const fs = require('fs');
286287
const path = require('path');
287-
const userDataDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
288+
const userDataDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
288289
fs.mkdirSync(userDataDir, { recursive: true });
289290

290291
// Support custom Chromium binary via GSTACK_CHROMIUM_PATH env var.
@@ -311,7 +312,7 @@ export class BrowserManager {
311312
// Replace Chromium's Dock icon with ours (Chromium's process owns the Dock icon)
312313
const iconCandidates = [
313314
path.join(__dirname, '..', '..', 'scripts', 'app', 'icon.icns'), // repo dev mode
314-
path.join(process.env.HOME || '', '.claude', 'skills', 'gstack', 'scripts', 'app', 'icon.icns'), // global install
315+
path.join(os.homedir(), '.claude', 'skills', 'gstack', 'scripts', 'app', 'icon.icns'), // global install
315316
];
316317
const iconSrc = iconCandidates.find(p => fs.existsSync(p));
317318
if (iconSrc) {
@@ -1159,7 +1160,7 @@ export class BrowserManager {
11591160
console.log('[browse] Handoff: extension not found — headed mode without side panel');
11601161
}
11611162

1162-
const userDataDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
1163+
const userDataDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
11631164
fs.mkdirSync(userDataDir, { recursive: true });
11641165

11651166
newContext = await chromium.launchPersistentContext(userDataDir, {

browse/src/cli.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111

1212
import * as fs from 'fs';
13+
import * as os from 'os';
1314
import * as path from 'path';
1415
import { safeUnlink, safeUnlinkQuiet, safeKill, isProcessAlive } from './error-handling';
1516
import { writeSecureFile, mkdirSecure } from './file-permissions';
@@ -472,17 +473,17 @@ async function sendCommand(state: ServerState, command: string, args: string[],
472473
/** Check if ngrok is installed and authenticated (native config or gstack env). */
473474
function isNgrokAvailable(): boolean {
474475
// Check gstack's own ngrok env
475-
const ngrokEnvPath = path.join(process.env.HOME || '/tmp', '.gstack', 'ngrok.env');
476+
const ngrokEnvPath = path.join(os.homedir(), '.gstack', 'ngrok.env');
476477
if (fs.existsSync(ngrokEnvPath)) return true;
477478

478479
// Check NGROK_AUTHTOKEN env var
479480
if (process.env.NGROK_AUTHTOKEN) return true;
480481

481482
// Check ngrok's native config (macOS + Linux)
482483
const ngrokConfigs = [
483-
path.join(process.env.HOME || '/tmp', 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
484-
path.join(process.env.HOME || '/tmp', '.config', 'ngrok', 'ngrok.yml'),
485-
path.join(process.env.HOME || '/tmp', '.ngrok2', 'ngrok.yml'),
484+
path.join(os.homedir(), 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
485+
path.join(os.homedir(), '.config', 'ngrok', 'ngrok.yml'),
486+
path.join(os.homedir(), '.ngrok2', 'ngrok.yml'),
486487
];
487488
for (const conf of ngrokConfigs) {
488489
try {
@@ -721,7 +722,7 @@ async function handlePairAgent(state: ServerState, args: string[]): Promise<void
721722
// Fallback to convention-based path
722723
}
723724

724-
const configDir = path.join(process.env.HOME || '/tmp', globalRoot);
725+
const configDir = path.join(os.homedir(), globalRoot);
725726
fs.mkdirSync(configDir, { recursive: true });
726727
const configFile = path.join(configDir, 'browse-remote.json');
727728
const configData = {
@@ -829,7 +830,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
829830
// Kill orphaned Chromium processes that may still hold the profile lock.
830831
// The server PID is the Bun process; Chromium is a child that can outlive it
831832
// if the server is killed abruptly (SIGKILL, crash, manual rm of state file).
832-
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
833+
const profileDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
833834
try {
834835
const singletonLock = path.join(profileDir, 'SingletonLock');
835836
const lockTarget = fs.readlinkSync(singletonLock); // e.g. "hostname-12345"
@@ -894,7 +895,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
894895
throw new Error(`sidebar-agent.ts not found at ${agentScript}`);
895896
}
896897
// Clear old agent queue
897-
const agentQueue = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent-queue.jsonl');
898+
const agentQueue = path.join(os.homedir(), '.gstack', 'sidebar-agent-queue.jsonl');
898899
try {
899900
mkdirSecure(path.dirname(agentQueue));
900901
writeSecureFile(agentQueue, '');
@@ -979,7 +980,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
979980
}
980981
}
981982
// Clean profile locks and state file
982-
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
983+
const profileDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
983984
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
984985
safeUnlinkQuiet(path.join(profileDir, lockFile));
985986
}

browse/src/server.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* Port: random 10000-60000 (or BROWSE_PORT env for debug override)
1414
*/
1515

16+
import * as os from 'os';
1617
import { writeSecureFile, appendSecureFile, mkdirSecure } from './file-permissions';
1718
import { BrowserManager } from './browser-manager';
1819
import { handleReadCommand } from './read-commands';
@@ -186,7 +187,7 @@ interface SidebarSession {
186187
lastActiveAt: string;
187188
}
188189

189-
const SESSIONS_DIR = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-sessions');
190+
const SESSIONS_DIR = path.join(os.homedir(), '.gstack', 'sidebar-sessions');
190191
const AGENT_TIMEOUT_MS = 300_000; // 5 minutes — multi-page tasks need time
191192
const MAX_QUEUE = 5;
192193

@@ -235,7 +236,7 @@ function findBrowseBin(): string {
235236
const candidates = [
236237
path.resolve(__dirname, '..', 'dist', 'browse'),
237238
path.resolve(__dirname, '..', '..', '.claude', 'skills', 'gstack', 'browse', 'dist', 'browse'),
238-
path.join(process.env.HOME || '', '.claude', 'skills', 'gstack', 'browse', 'dist', 'browse'),
239+
path.join(os.homedir(), '.claude', 'skills', 'gstack', 'browse', 'dist', 'browse'),
239240
];
240241
for (const c of candidates) {
241242
try { if (fs.existsSync(c)) return c; } catch (err: any) {
@@ -248,7 +249,7 @@ function findBrowseBin(): string {
248249
const BROWSE_BIN = findBrowseBin();
249250

250251
function findClaudeBin(): string | null {
251-
const home = process.env.HOME || '';
252+
const home = os.homedir();
252253
const candidates = [
253254
// Conductor app bundled binary (not a symlink — works reliably)
254255
path.join(home, 'Library', 'Application Support', 'com.conductor.app', 'bin', 'claude'),
@@ -382,7 +383,7 @@ function createWorktree(sessionId: string): string | null {
382383
if (gitCheck.exitCode !== 0) return null;
383384
const repoRoot = gitCheck.stdout.toString().trim();
384385

385-
const worktreeDir = path.join(process.env.HOME || '/tmp', '.gstack', 'worktrees', sessionId.slice(0, 8));
386+
const worktreeDir = path.join(os.homedir(), '.gstack', 'worktrees', sessionId.slice(0, 8));
386387

387388
// Clean up if dir exists from prior crash
388389
if (fs.existsSync(worktreeDir)) {
@@ -633,7 +634,7 @@ function spawnClaude(userMessage: string, extensionUrl?: string | null, forTabId
633634
// fails with ENOENT on everything, including /bin/bash). Instead,
634635
// write the command to a queue file that the sidebar-agent process
635636
// (running as non-compiled bun) picks up and spawns claude.
636-
const agentQueue = process.env.SIDEBAR_QUEUE_PATH || path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent-queue.jsonl');
637+
const agentQueue = process.env.SIDEBAR_QUEUE_PATH || path.join(os.homedir(), '.gstack', 'sidebar-agent-queue.jsonl');
637638
const gstackDir = path.dirname(agentQueue);
638639
const entry = JSON.stringify({
639640
ts: new Date().toISOString(),
@@ -674,7 +675,7 @@ function killAgent(targetTabId?: number | null): void {
674675
// Using per-tab files prevents race conditions where one agent's cancel
675676
// signal is consumed by a different tab's agent in concurrent mode.
676677
// When targetTabId is provided, only that tab's agent is cancelled.
677-
const cancelDir = path.join(process.env.HOME || '/tmp', '.gstack');
678+
const cancelDir = path.join(os.homedir(), '.gstack');
678679
const tabId = targetTabId ?? agentTabId ?? 0;
679680
const cancelFile = path.join(cancelDir, `sidebar-agent-cancel-${tabId}`);
680681
try {
@@ -1302,7 +1303,7 @@ async function shutdown(exitCode: number = 0) {
13021303
await browserManager.close();
13031304

13041305
// Clean up Chromium profile locks (prevent SingletonLock on next launch)
1305-
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
1306+
const profileDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
13061307
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
13071308
safeUnlinkQuiet(path.join(profileDir, lockFile));
13081309
}
@@ -1365,7 +1366,7 @@ function emergencyCleanup() {
13651366
console.error('[browse] Emergency: failed to save session:', err.message);
13661367
}
13671368
// Clean Chromium profile locks
1368-
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
1369+
const profileDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
13691370
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
13701371
safeUnlinkQuiet(path.join(profileDir, lockFile));
13711372
}
@@ -1421,7 +1422,7 @@ async function start() {
14211422
const welcomePath = (() => {
14221423
// Check project-local designs first, then global
14231424
const slug = process.env.GSTACK_SLUG || 'unknown';
1424-
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
1425+
const homeDir = os.homedir();
14251426
const projectWelcome = `${homeDir}/.gstack/projects/${slug}/designs/welcome-page-20260331/finalized.html`;
14261427
if (fs.existsSync(projectWelcome)) return projectWelcome;
14271428
// Fallback: built-in welcome page from gstack install
@@ -1679,7 +1680,7 @@ async function start() {
16791680
// Read ngrok authtoken: env var > ~/.gstack/ngrok.env > ngrok native config
16801681
let authtoken = process.env.NGROK_AUTHTOKEN;
16811682
if (!authtoken) {
1682-
const ngrokEnvPath = path.join(process.env.HOME || '', '.gstack', 'ngrok.env');
1683+
const ngrokEnvPath = path.join(os.homedir(), '.gstack', 'ngrok.env');
16831684
if (fs.existsSync(ngrokEnvPath)) {
16841685
const envContent = fs.readFileSync(ngrokEnvPath, 'utf-8');
16851686
const match = envContent.match(/^NGROK_AUTHTOKEN=(.+)$/m);
@@ -1689,9 +1690,9 @@ async function start() {
16891690
if (!authtoken) {
16901691
// Check ngrok's native config files
16911692
const ngrokConfigs = [
1692-
path.join(process.env.HOME || '', 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
1693-
path.join(process.env.HOME || '', '.config', 'ngrok', 'ngrok.yml'),
1694-
path.join(process.env.HOME || '', '.ngrok2', 'ngrok.yml'),
1693+
path.join(os.homedir(), 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
1694+
path.join(os.homedir(), '.config', 'ngrok', 'ngrok.yml'),
1695+
path.join(os.homedir(), '.ngrok2', 'ngrok.yml'),
16951696
];
16961697
for (const conf of ngrokConfigs) {
16971698
try {
@@ -2503,7 +2504,7 @@ async function start() {
25032504
// Read ngrok authtoken from env or config file
25042505
let authtoken = process.env.NGROK_AUTHTOKEN;
25052506
if (!authtoken) {
2506-
const ngrokEnvPath = path.join(process.env.HOME || '', '.gstack', 'ngrok.env');
2507+
const ngrokEnvPath = path.join(os.homedir(), '.gstack', 'ngrok.env');
25072508
if (fs.existsSync(ngrokEnvPath)) {
25082509
const envContent = fs.readFileSync(ngrokEnvPath, 'utf-8');
25092510
const match = envContent.match(/^NGROK_AUTHTOKEN=(.+)$/m);

browse/src/sidebar-agent.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import { spawn } from 'child_process';
1313
import * as fs from 'fs';
14+
import * as os from 'os';
1415
import * as path from 'path';
1516
import { safeUnlink } from './error-handling';
1617
import { writeSecureFile, mkdirSecure, restrictFilePermissions } from './file-permissions';
@@ -27,14 +28,14 @@ import {
2728
type ToolCallInput,
2829
} from './security-classifier';
2930

30-
const QUEUE = process.env.SIDEBAR_QUEUE_PATH || path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent-queue.jsonl');
31+
const QUEUE = process.env.SIDEBAR_QUEUE_PATH || path.join(os.homedir(), '.gstack', 'sidebar-agent-queue.jsonl');
3132
const KILL_FILE = path.join(path.dirname(QUEUE), 'sidebar-agent-kill');
3233
const SERVER_PORT = parseInt(process.env.BROWSE_SERVER_PORT || '34567', 10);
3334
const SERVER_URL = `http://127.0.0.1:${SERVER_PORT}`;
3435
const POLL_MS = 200; // 200ms poll — keeps time-to-first-token low
3536
const B = process.env.BROWSE_BIN || path.resolve(__dirname, '../../.claude/skills/gstack/browse/dist/browse');
3637

37-
const CANCEL_DIR = path.join(process.env.HOME || '/tmp', '.gstack');
38+
const CANCEL_DIR = path.join(os.homedir(), '.gstack');
3839
function cancelFileForTab(tabId: number): string {
3940
return path.join(CANCEL_DIR, `sidebar-agent-cancel-${tabId}`);
4041
}
@@ -130,7 +131,7 @@ async function refreshToken(): Promise<string | null> {
130131
// Read token from state file (same-user, mode 0o600) instead of /health
131132
try {
132133
const stateFile = process.env.BROWSE_STATE_FILE ||
133-
path.join(process.env.HOME || '/tmp', '.gstack', 'browse.json');
134+
path.join(os.homedir(), '.gstack', 'browse.json');
134135
const data = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
135136
authToken = data.token || null;
136137
return authToken;

0 commit comments

Comments
 (0)