Skip to content

Commit e92fcc6

Browse files
author
dave horner
committed
feat(windows): update development scripts to use cross-env for environment variable management in various packages
1 parent c1980c2 commit e92fcc6

File tree

18 files changed

+769
-385
lines changed

18 files changed

+769
-385
lines changed

.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Agor Playground (Recommended - Fast npm install)",
3-
// Cache bust: 2025-11-15-v20 - Bump to agor-live v0.8.0
3+
// Cache bust: 2025-11-15-v20 - Bump to agor-live v0.8.2
44
"build": {
55
"dockerfile": "playground/Dockerfile",
66
"context": ".."

apps/agor-cli/.pnpmfile.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "cross-env",
3+
"version": "7.0.3"
4+
}

apps/agor-cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
},
99
"scripts": {
1010
"build": "tsup",
11-
"dev": "NODE_NO_WARNINGS=1 tsx bin/dev.ts",
11+
"dev": "cross-env NODE_NO_WARNINGS=1 tsx bin/dev.ts",
1212
"clean": "rm -rf dist",
1313
"typecheck": "tsc --noEmit"
1414
},

apps/agor-daemon/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"main": "./dist/index.js",
77
"scripts": {
88
"build": "tsup",
9-
"dev": "concurrently -k -n core,daemon -c cyan,green \"pnpm --filter @agor/core dev\" \"./node_modules/.bin/tsx watch --clear-screen=false --ignore 'node_modules/**' src/index.ts\"",
9+
"dev": "concurrently -k -n core,daemon -c cyan,green \"pnpm --filter @agor/core dev\" \"cross-env-shell .\\node_modules\\.bin\\tsx watch --clear-screen=false --ignore 'node_modules/**' src/index.ts\"",
1010
"dev:daemon-only": "tsx watch --clear-screen=false --ignore 'node_modules/**' src/index.ts",
1111
"start": "node dist/index.js",
1212
"clean": "rm -rf dist",

apps/agor-daemon/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,8 @@ async function main() {
646646
if (DB_PATH.startsWith('file:')) {
647647
// Extract file path from DB_PATH (remove 'file:' prefix and expand ~)
648648
const dbFilePath = extractDbFilePath(DB_PATH);
649-
const dbDir = dbFilePath.substring(0, dbFilePath.lastIndexOf('/'));
649+
const path = await import('node:path');
650+
const dbDir = path.dirname(dbFilePath);
650651

651652
// Ensure database directory exists
652653
const { mkdir, access } = await import('node:fs/promises');

apps/agor-daemon/src/services/terminals.ts

Lines changed: 191 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import os from 'node:os';
1717
import { resolveUserEnvironment } from '@agor/core/config';
1818
import { type Database, WorktreeRepository } from '@agor/core/db';
1919
import type { Application } from '@agor/core/feathers';
20-
import type { UserID, WorktreeID } from '@agor/core/types';
20+
import type { AuthenticatedParams, UserID, WorktreeID } from '@agor/core/types';
2121
import type { IPty } from '@homebridge/node-pty-prebuilt-multiarch';
2222
import * as pty from '@homebridge/node-pty-prebuilt-multiarch';
2323

@@ -39,6 +39,7 @@ interface CreateTerminalData {
3939
cols?: number;
4040
userId?: UserID; // User context for env resolution
4141
worktreeId?: WorktreeID; // Worktree context for tmux integration
42+
useTmux?: boolean; // Optional flag to disable tmux even when available
4243
}
4344

4445
interface ResizeTerminalData {
@@ -94,6 +95,66 @@ function findTmuxWindow(sessionName: string, windowName: string): number | null
9495
}
9596
}
9697

98+
/**
99+
* Ensure tmux session is configured to pass OSC hyperlinks and other rich output.
100+
*/
101+
function configureTmuxSession(sessionName: string): void {
102+
try {
103+
execSync(`tmux set-option -t "${sessionName}" -g allow-passthrough on`, { stdio: 'pipe' });
104+
} catch (error) {
105+
const message = error instanceof Error ? error.message : String(error);
106+
console.warn(
107+
`⚠️ Failed to enable tmux allow-passthrough for session ${sessionName}: ${message}`
108+
);
109+
}
110+
111+
try {
112+
execSync(`tmux set-option -t "${sessionName}" -g default-terminal 'tmux-256color'`, {
113+
stdio: 'pipe',
114+
});
115+
} catch (error) {
116+
const message = error instanceof Error ? error.message : String(error);
117+
console.warn(`⚠️ Failed to set tmux default-terminal for ${sessionName}: ${message}`);
118+
}
119+
120+
try {
121+
execSync(`tmux set-option -ga terminal-features 'xterm*:allow-passthrough'`, { stdio: 'pipe' });
122+
} catch (error) {
123+
const message = error instanceof Error ? error.message : String(error);
124+
console.warn(`⚠️ Failed to advertise tmux allow-passthrough feature: ${message}`);
125+
}
126+
127+
try {
128+
execSync(`tmux set-option -ga terminal-features 'tmux-256color:hyperlinks,RGB,extkeys'`, {
129+
stdio: 'pipe',
130+
});
131+
} catch (error) {
132+
const message = error instanceof Error ? error.message : String(error);
133+
console.warn(`⚠️ Failed to advertise tmux tmux-256color features: ${message}`);
134+
}
135+
136+
try {
137+
execSync(`tmux set-option -ga terminal-features 'xterm*:hyperlinks'`, { stdio: 'pipe' });
138+
} catch (error) {
139+
const message = error instanceof Error ? error.message : String(error);
140+
console.warn(`⚠️ Failed to enable tmux hyperlink feature: ${message}`);
141+
}
142+
143+
try {
144+
execSync(`tmux set-option -as terminal-overrides ',*:allow-passthrough'`, { stdio: 'pipe' });
145+
} catch (error) {
146+
const message = error instanceof Error ? error.message : String(error);
147+
console.warn(`⚠️ Failed to configure tmux allow-passthrough override: ${message}`);
148+
}
149+
150+
try {
151+
execSync(`tmux set-option -as terminal-overrides ',*:hyperlinks'`, { stdio: 'pipe' });
152+
} catch (error) {
153+
const message = error instanceof Error ? error.message : String(error);
154+
console.warn(`⚠️ Failed to configure tmux hyperlink override: ${message}`);
155+
}
156+
}
157+
97158
/**
98159
* Terminals service - manages PTY sessions
99160
*/
@@ -102,13 +163,17 @@ export class TerminalsService {
102163
private app: Application;
103164
private db: Database;
104165
private hasTmux: boolean;
166+
private forceTmux: boolean;
105167

106168
constructor(app: Application, db: Database) {
107169
this.app = app;
108170
this.db = db;
109171
this.hasTmux = isTmuxAvailable();
172+
this.forceTmux = process.env.AGOR_DISABLE_TMUX !== 'true';
110173

111-
if (this.hasTmux) {
174+
if (!this.forceTmux) {
175+
console.log('ℹ️ tmux disabled via AGOR_DISABLE_TMUX=true - using direct PTY sessions');
176+
} else if (this.hasTmux) {
112177
console.log('\x1b[36m✅ tmux detected\x1b[0m - persistent terminal sessions enabled');
113178
} else {
114179
console.log('ℹ️ tmux not found - using ephemeral terminal sessions');
@@ -118,14 +183,24 @@ export class TerminalsService {
118183
/**
119184
* Create a new terminal session
120185
*/
121-
async create(data: CreateTerminalData): Promise<{
186+
async create(
187+
data: CreateTerminalData,
188+
params?: AuthenticatedParams
189+
): Promise<{
122190
terminalId: string;
123191
cwd: string;
124192
tmuxSession?: string;
125193
tmuxReused?: boolean;
126194
worktreeName?: string;
127195
}> {
128196
const terminalId = `term-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
197+
const authenticatedUserId = params?.user?.user_id as UserID | undefined;
198+
const resolvedUserId = data.userId ?? authenticatedUserId;
199+
const userSessionSuffix = (() => {
200+
if (!resolvedUserId) return 'shared';
201+
const sanitized = resolvedUserId.replace(/[^a-zA-Z0-9_-]/g, '');
202+
return sanitized.length > 0 ? sanitized : 'user';
203+
})();
129204

130205
// Resolve worktree context if provided
131206
let worktree = null;
@@ -142,20 +217,25 @@ export class TerminalsService {
142217
}
143218

144219
// Determine shell and tmux configuration
145-
let shell: string;
146-
let shellArgs: string[] = [];
220+
const defaultShell = data.shell || (os.platform() === 'win32' ? 'powershell.exe' : 'bash');
221+
const defaultShellArgs: string[] = [];
222+
let shell: string = defaultShell;
223+
let shellArgs: string[] = [...defaultShellArgs];
147224
let tmuxSession: string | undefined;
148225
let tmuxReused = false;
149226

150-
if (this.hasTmux && worktree) {
227+
const tmuxRequested = data.useTmux !== false;
228+
229+
if (this.hasTmux && this.forceTmux && worktree && tmuxRequested) {
151230
// Use single shared tmux session with one window per worktree
152-
tmuxSession = 'agor';
231+
tmuxSession = `agor-${userSessionSuffix}`;
153232
const sessionExists = tmuxSessionExists(tmuxSession);
154233
const windowName = worktreeName || 'unnamed';
155234

156235
shell = 'tmux';
157236

158237
if (sessionExists) {
238+
configureTmuxSession(tmuxSession);
159239
// Session exists - check if this worktree has a window
160240
const windowIndex = findTmuxWindow(tmuxSession, windowName);
161241

@@ -199,44 +279,135 @@ export class TerminalsService {
199279
'set-option',
200280
'-t',
201281
tmuxSession,
282+
'default-terminal',
283+
'tmux-256color',
284+
';',
285+
'set-option',
286+
'-t',
287+
tmuxSession,
202288
'status-style',
203289
'bg=#2e9a92,fg=#000000',
290+
';',
291+
'set-option',
292+
'-t',
293+
tmuxSession,
294+
'allow-passthrough',
295+
'on',
296+
';',
297+
'set-option',
298+
'-ga',
299+
'terminal-features',
300+
'xterm*:hyperlinks',
301+
';',
302+
'set-option',
303+
'-ga',
304+
'terminal-features',
305+
'tmux-256color:hyperlinks,RGB,extkeys',
306+
';',
307+
'set-option',
308+
'-ga',
309+
'terminal-features',
310+
'xterm*:allow-passthrough',
311+
';',
312+
'set-option',
313+
'-as',
314+
'terminal-overrides',
315+
',*:allow-passthrough',
316+
';',
317+
'set-option',
318+
'-as',
319+
'terminal-overrides',
320+
',*:hyperlinks',
204321
];
205322
tmuxReused = false;
206323
console.log(
207324
`\x1b[36m🚀 Creating tmux session:\x1b[0m ${tmuxSession} with window (${windowName}) + teal theme`
208325
);
209326
}
210327
} else {
328+
if (this.hasTmux && this.forceTmux && !tmuxRequested) {
329+
console.log('ℹ️ tmux disabled for this terminal session (user toggle)');
330+
}
211331
// Fallback to regular shell
212-
shell = data.shell || (os.platform() === 'win32' ? 'powershell.exe' : 'bash');
332+
shell = defaultShell;
333+
shellArgs = [...defaultShellArgs];
213334
}
214335

215336
// Resolve environment with user env vars if userId provided
216-
let env: Record<string, string> = process.env as Record<string, string>;
217-
if (data.userId) {
218-
env = await resolveUserEnvironment(data.userId, this.db);
337+
let env: Record<string, string> = { ...(process.env as Record<string, string>) };
338+
if (resolvedUserId) {
339+
const userEnv = await resolveUserEnvironment(resolvedUserId, this.db);
219340
console.log(
220-
`🔐 Loaded ${Object.keys(env).length} env vars for user ${data.userId.substring(0, 8)}`
341+
`🔐 Loaded ${Object.keys(userEnv).length} env vars for user ${resolvedUserId.substring(0, 8)}`
221342
);
343+
env = { ...env, ...userEnv };
344+
}
345+
env = { ...env };
346+
347+
// Ensure terminal capabilities advertised to downstream processes
348+
if (!env.TERM) {
349+
env.TERM = 'xterm-256color';
350+
}
351+
if (!env.COLORTERM) {
352+
env.COLORTERM = 'truecolor';
353+
}
354+
if (!env.ENABLE_HYPERLINKS) {
355+
env.ENABLE_HYPERLINKS = '1';
356+
}
357+
if (!env.RICH_FORCE_COLOR) {
358+
env.RICH_FORCE_COLOR = '1';
359+
}
360+
if (!env.RICH_FORCE_HYPERLINK) {
361+
env.RICH_FORCE_HYPERLINK = '1';
362+
}
363+
if (!env.LANG) {
364+
env.LANG = 'C.UTF-8';
365+
}
366+
if (!env.LC_ALL) {
367+
env.LC_ALL = env.LANG;
368+
}
369+
if (!env.LC_CTYPE) {
370+
env.LC_CTYPE = env.LANG;
222371
}
223372

224373
// Spawn PTY process
225-
const ptyProcess = pty.spawn(shell, shellArgs, {
226-
name: 'xterm-color',
227-
cols: data.cols || 80,
228-
rows: data.rows || 30,
229-
cwd,
230-
env, // Use resolved environment
231-
});
374+
let ptyProcess: IPty;
375+
try {
376+
ptyProcess = pty.spawn(shell, shellArgs, {
377+
name: 'xterm-256color',
378+
cols: data.cols || 80,
379+
rows: data.rows || 30,
380+
cwd,
381+
env, // Use resolved environment
382+
});
383+
} catch (error) {
384+
if (shell === 'tmux') {
385+
const message = error instanceof Error ? error.message : String(error);
386+
console.warn(`⚠️ Failed to launch tmux session, falling back to direct shell: ${message}`);
387+
this.hasTmux = false;
388+
tmuxSession = undefined;
389+
tmuxReused = false;
390+
shell = defaultShell;
391+
shellArgs = [...defaultShellArgs];
392+
ptyProcess = pty.spawn(shell, shellArgs, {
393+
name: 'xterm-256color',
394+
cols: data.cols || 80,
395+
rows: data.rows || 30,
396+
cwd,
397+
env,
398+
});
399+
} else {
400+
throw error;
401+
}
402+
}
232403

233404
// Store session
234405
this.sessions.set(terminalId, {
235406
terminalId,
236407
pty: ptyProcess,
237408
shell,
238409
cwd,
239-
userId: data.userId,
410+
userId: resolvedUserId,
240411
worktreeId: data.worktreeId,
241412
tmuxSession,
242413
createdAt: new Date(),

apps/agor-ui/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
"react-router-dom": "^7.9.6",
3636
"reactflow": "^11.11.4",
3737
"streamdown": "^1.5.1",
38-
"xterm": "^5.3.0"
38+
"xterm": "^5.3.0",
39+
"xterm-addon-web-links": "^0.9.0"
3940
},
4041
"devDependencies": {
4142
"@chromatic-com/storybook": "^4.1.1",

apps/agor-ui/src/components/App/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,8 @@ export const App: React.FC<AppProps> = ({
248248
// Update favicon based on session activity on current board
249249
useFaviconStatus(currentBoardId, sessionsByWorktree, mapToArray(boardObjectById));
250250

251-
// Check if event stream is enabled in user preferences
252-
const eventStreamEnabled = user?.preferences?.eventStream?.enabled ?? false;
251+
// Check if event stream is enabled in user preferences (default: true)
252+
const eventStreamEnabled = user?.preferences?.eventStream?.enabled ?? true;
253253

254254
// Event stream hook - only captures events when panel is open
255255
const { events, clearEvents } = useEventStream({
@@ -549,6 +549,7 @@ export const App: React.FC<AppProps> = ({
549549
userById={userById}
550550
currentUserId={user?.user_id}
551551
selectedSessionId={selectedSessionId}
552+
currentBoard={currentBoard}
552553
worktreeActions={{
553554
onSessionClick: setSelectedSessionId,
554555
onCreateSession: (worktreeId) => setNewSessionWorktreeId(worktreeId),

0 commit comments

Comments
 (0)