Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions browse/src/browser-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* restores state. Falls back to clean slate on any failure.
*/

import * as os from 'os';
import { chromium, type Browser, type BrowserContext, type BrowserContextOptions, type Page, type Locator, type Cookie } from 'playwright';
import { addConsoleEntry, addNetworkEntry, addDialogEntry, networkBuffer, type DialogEntry } from './buffers';
import { validateNavigationUrl } from './url-validation';
Expand Down Expand Up @@ -139,7 +140,7 @@ export class BrowserManager {
// Relative to this source file (dev mode: browse/src/ -> ../../extension)
path.resolve(__dirname, '..', '..', 'extension'),
// Global gstack install
path.join(process.env.HOME || '', '.claude', 'skills', 'gstack', 'extension'),
path.join(os.homedir(), '.claude', 'skills', 'gstack', 'extension'),
// Git repo root (detected via BROWSE_STATE_FILE location)
(() => {
const stateFile = process.env.BROWSE_STATE_FILE || '';
Expand Down Expand Up @@ -266,7 +267,7 @@ export class BrowserManager {
if (authToken) {
const fs = require('fs');
const path = require('path');
const gstackDir = path.join(process.env.HOME || '/tmp', '.gstack');
const gstackDir = path.join(os.homedir(), '.gstack');
fs.mkdirSync(gstackDir, { recursive: true });
const authFile = path.join(gstackDir, '.auth.json');
try {
Expand All @@ -283,7 +284,7 @@ export class BrowserManager {
// so we use Playwright's bundled Chromium which reliably loads extensions.
const fs = require('fs');
const path = require('path');
const userDataDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
const userDataDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
fs.mkdirSync(userDataDir, { recursive: true });

// Support custom Chromium binary via GSTACK_CHROMIUM_PATH env var.
Expand All @@ -310,7 +311,7 @@ export class BrowserManager {
// Replace Chromium's Dock icon with ours (Chromium's process owns the Dock icon)
const iconCandidates = [
path.join(__dirname, '..', '..', 'scripts', 'app', 'icon.icns'), // repo dev mode
path.join(process.env.HOME || '', '.claude', 'skills', 'gstack', 'scripts', 'app', 'icon.icns'), // global install
path.join(os.homedir(), '.claude', 'skills', 'gstack', 'scripts', 'app', 'icon.icns'), // global install
];
const iconSrc = iconCandidates.find(p => fs.existsSync(p));
if (iconSrc) {
Expand Down Expand Up @@ -1158,7 +1159,7 @@ export class BrowserManager {
console.log('[browse] Handoff: extension not found — headed mode without side panel');
}

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

newContext = await chromium.launchPersistentContext(userDataDir, {
Expand Down
17 changes: 9 additions & 8 deletions browse/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { safeUnlink, safeUnlinkQuiet, safeKill, isProcessAlive } from './error-handling';
import { resolveConfig, ensureStateDir, readVersionHash } from './config';
Expand Down Expand Up @@ -471,17 +472,17 @@ async function sendCommand(state: ServerState, command: string, args: string[],
/** Check if ngrok is installed and authenticated (native config or gstack env). */
function isNgrokAvailable(): boolean {
// Check gstack's own ngrok env
const ngrokEnvPath = path.join(process.env.HOME || '/tmp', '.gstack', 'ngrok.env');
const ngrokEnvPath = path.join(os.homedir(), '.gstack', 'ngrok.env');
if (fs.existsSync(ngrokEnvPath)) return true;

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

// Check ngrok's native config (macOS + Linux)
const ngrokConfigs = [
path.join(process.env.HOME || '/tmp', 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
path.join(process.env.HOME || '/tmp', '.config', 'ngrok', 'ngrok.yml'),
path.join(process.env.HOME || '/tmp', '.ngrok2', 'ngrok.yml'),
path.join(os.homedir(), 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
path.join(os.homedir(), '.config', 'ngrok', 'ngrok.yml'),
path.join(os.homedir(), '.ngrok2', 'ngrok.yml'),
];
for (const conf of ngrokConfigs) {
try {
Expand Down Expand Up @@ -720,7 +721,7 @@ async function handlePairAgent(state: ServerState, args: string[]): Promise<void
// Fallback to convention-based path
}

const configDir = path.join(process.env.HOME || '/tmp', globalRoot);
const configDir = path.join(os.homedir(), globalRoot);
fs.mkdirSync(configDir, { recursive: true });
const configFile = path.join(configDir, 'browse-remote.json');
const configData = {
Expand Down Expand Up @@ -828,7 +829,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
// Kill orphaned Chromium processes that may still hold the profile lock.
// The server PID is the Bun process; Chromium is a child that can outlive it
// if the server is killed abruptly (SIGKILL, crash, manual rm of state file).
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
const profileDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
try {
const singletonLock = path.join(profileDir, 'SingletonLock');
const lockTarget = fs.readlinkSync(singletonLock); // e.g. "hostname-12345"
Expand Down Expand Up @@ -893,7 +894,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
throw new Error(`sidebar-agent.ts not found at ${agentScript}`);
}
// Clear old agent queue
const agentQueue = path.join(process.env.HOME || '/tmp', '.gstack', 'sidebar-agent-queue.jsonl');
const agentQueue = path.join(os.homedir(), '.gstack', 'sidebar-agent-queue.jsonl');
try {
fs.mkdirSync(path.dirname(agentQueue), { recursive: true, mode: 0o700 });
fs.writeFileSync(agentQueue, '', { mode: 0o600 });
Expand Down Expand Up @@ -978,7 +979,7 @@ Refs: After 'snapshot', use @e1, @e2... as selectors:
}
}
// Clean profile locks and state file
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
const profileDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
safeUnlinkQuiet(path.join(profileDir, lockFile));
}
Expand Down
29 changes: 15 additions & 14 deletions browse/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* Port: random 10000-60000 (or BROWSE_PORT env for debug override)
*/

import * as os from 'os';
import { BrowserManager } from './browser-manager';
import { handleReadCommand } from './read-commands';
import { handleWriteCommand } from './write-commands';
Expand Down Expand Up @@ -185,7 +186,7 @@ interface SidebarSession {
lastActiveAt: string;
}

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

Expand Down Expand Up @@ -234,7 +235,7 @@ function findBrowseBin(): string {
const candidates = [
path.resolve(__dirname, '..', 'dist', 'browse'),
path.resolve(__dirname, '..', '..', '.claude', 'skills', 'gstack', 'browse', 'dist', 'browse'),
path.join(process.env.HOME || '', '.claude', 'skills', 'gstack', 'browse', 'dist', 'browse'),
path.join(os.homedir(), '.claude', 'skills', 'gstack', 'browse', 'dist', 'browse'),
];
for (const c of candidates) {
try { if (fs.existsSync(c)) return c; } catch (err: any) {
Expand All @@ -247,7 +248,7 @@ function findBrowseBin(): string {
const BROWSE_BIN = findBrowseBin();

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

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

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

// Clean up Chromium profile locks (prevent SingletonLock on next launch)
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
const profileDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
safeUnlinkQuiet(path.join(profileDir, lockFile));
}
Expand Down Expand Up @@ -1367,7 +1368,7 @@ function emergencyCleanup() {
console.error('[browse] Emergency: failed to save session:', err.message);
}
// Clean Chromium profile locks
const profileDir = path.join(process.env.HOME || '/tmp', '.gstack', 'chromium-profile');
const profileDir = path.join(os.homedir(), '.gstack', 'chromium-profile');
for (const lockFile of ['SingletonLock', 'SingletonSocket', 'SingletonCookie']) {
safeUnlinkQuiet(path.join(profileDir, lockFile));
}
Expand Down Expand Up @@ -1423,7 +1424,7 @@ async function start() {
const welcomePath = (() => {
// Check project-local designs first, then global
const slug = process.env.GSTACK_SLUG || 'unknown';
const homeDir = process.env.HOME || process.env.USERPROFILE || '/tmp';
const homeDir = os.homedir();
const projectWelcome = `${homeDir}/.gstack/projects/${slug}/designs/welcome-page-20260331/finalized.html`;
if (fs.existsSync(projectWelcome)) return projectWelcome;
// Fallback: built-in welcome page from gstack install
Expand Down Expand Up @@ -1681,7 +1682,7 @@ async function start() {
// Read ngrok authtoken: env var > ~/.gstack/ngrok.env > ngrok native config
let authtoken = process.env.NGROK_AUTHTOKEN;
if (!authtoken) {
const ngrokEnvPath = path.join(process.env.HOME || '', '.gstack', 'ngrok.env');
const ngrokEnvPath = path.join(os.homedir(), '.gstack', 'ngrok.env');
if (fs.existsSync(ngrokEnvPath)) {
const envContent = fs.readFileSync(ngrokEnvPath, 'utf-8');
const match = envContent.match(/^NGROK_AUTHTOKEN=(.+)$/m);
Expand All @@ -1691,9 +1692,9 @@ async function start() {
if (!authtoken) {
// Check ngrok's native config files
const ngrokConfigs = [
path.join(process.env.HOME || '', 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
path.join(process.env.HOME || '', '.config', 'ngrok', 'ngrok.yml'),
path.join(process.env.HOME || '', '.ngrok2', 'ngrok.yml'),
path.join(os.homedir(), 'Library', 'Application Support', 'ngrok', 'ngrok.yml'),
path.join(os.homedir(), '.config', 'ngrok', 'ngrok.yml'),
path.join(os.homedir(), '.ngrok2', 'ngrok.yml'),
];
for (const conf of ngrokConfigs) {
try {
Expand Down Expand Up @@ -2505,7 +2506,7 @@ async function start() {
// Read ngrok authtoken from env or config file
let authtoken = process.env.NGROK_AUTHTOKEN;
if (!authtoken) {
const ngrokEnvPath = path.join(process.env.HOME || '', '.gstack', 'ngrok.env');
const ngrokEnvPath = path.join(os.homedir(), '.gstack', 'ngrok.env');
if (fs.existsSync(ngrokEnvPath)) {
const envContent = fs.readFileSync(ngrokEnvPath, 'utf-8');
const match = envContent.match(/^NGROK_AUTHTOKEN=(.+)$/m);
Expand Down
7 changes: 4 additions & 3 deletions browse/src/sidebar-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import { spawn } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { safeUnlink } from './error-handling';
import {
Expand All @@ -26,14 +27,14 @@ import {
type ToolCallInput,
} from './security-classifier';

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

const CANCEL_DIR = path.join(process.env.HOME || '/tmp', '.gstack');
const CANCEL_DIR = path.join(os.homedir(), '.gstack');
function cancelFileForTab(tabId: number): string {
return path.join(CANCEL_DIR, `sidebar-agent-cancel-${tabId}`);
}
Expand Down Expand Up @@ -129,7 +130,7 @@ async function refreshToken(): Promise<string | null> {
// Read token from state file (same-user, mode 0o600) instead of /health
try {
const stateFile = process.env.BROWSE_STATE_FILE ||
path.join(process.env.HOME || '/tmp', '.gstack', 'browse.json');
path.join(os.homedir(), '.gstack', 'browse.json');
const data = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
authToken = data.token || null;
return authToken;
Expand Down
3 changes: 2 additions & 1 deletion design/prototype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
*/

import fs from "fs";
import os from "os";
import path from "path";

const API_KEY = process.env.OPENAI_API_KEY
|| JSON.parse(fs.readFileSync(path.join(process.env.HOME!, ".gstack/openai.json"), "utf-8")).api_key;
|| JSON.parse(fs.readFileSync(path.join(os.homedir(), ".gstack/openai.json"), "utf-8")).api_key;

if (!API_KEY) {
console.error("No API key found. Set OPENAI_API_KEY or save to ~/.gstack/openai.json");
Expand Down
3 changes: 2 additions & 1 deletion design/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
*/

import fs from "fs";
import os from "os";
import path from "path";

const CONFIG_PATH = path.join(process.env.HOME || "~", ".gstack", "openai.json");
const CONFIG_PATH = path.join(os.homedir(), ".gstack", "openai.json");

export function resolveApiKey(): string | null {
// 1. Check ~/.gstack/openai.json
Expand Down