brosh 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/brosh_brandmark.svg +3 -0
- package/brosh_logo.svg +27 -0
- package/cli_icon.svg +52 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +138 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +618 -0
- package/dist/index.js.map +1 -0
- package/dist/lib.d.ts +25 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +28 -0
- package/dist/lib.js.map +1 -0
- package/dist/mode-selector.d.ts +7 -0
- package/dist/mode-selector.d.ts.map +1 -0
- package/dist/mode-selector.js +138 -0
- package/dist/mode-selector.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +79 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/recording/index.d.ts +4 -0
- package/dist/recording/index.d.ts.map +1 -0
- package/dist/recording/index.js +3 -0
- package/dist/recording/index.js.map +1 -0
- package/dist/recording/manager.d.ts +62 -0
- package/dist/recording/manager.d.ts.map +1 -0
- package/dist/recording/manager.js +123 -0
- package/dist/recording/manager.js.map +1 -0
- package/dist/recording/recorder.d.ts +95 -0
- package/dist/recording/recorder.d.ts.map +1 -0
- package/dist/recording/recorder.js +330 -0
- package/dist/recording/recorder.js.map +1 -0
- package/dist/recording/types.d.ts +65 -0
- package/dist/recording/types.d.ts.map +1 -0
- package/dist/recording/types.js +2 -0
- package/dist/recording/types.js.map +1 -0
- package/dist/sandbox/ModeSelector.d.ts +2 -0
- package/dist/sandbox/ModeSelector.d.ts.map +1 -0
- package/dist/sandbox/ModeSelector.js +2 -0
- package/dist/sandbox/ModeSelector.js.map +1 -0
- package/dist/sandbox/config.d.ts +46 -0
- package/dist/sandbox/config.d.ts.map +1 -0
- package/dist/sandbox/config.js +144 -0
- package/dist/sandbox/config.js.map +1 -0
- package/dist/sandbox/controller.d.ts +72 -0
- package/dist/sandbox/controller.d.ts.map +1 -0
- package/dist/sandbox/controller.js +208 -0
- package/dist/sandbox/controller.js.map +1 -0
- package/dist/sandbox/index.d.ts +6 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +4 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/mode-prompt.d.ts +10 -0
- package/dist/sandbox/mode-prompt.d.ts.map +1 -0
- package/dist/sandbox/mode-prompt.js +130 -0
- package/dist/sandbox/mode-prompt.js.map +1 -0
- package/dist/sandbox/prompt.d.ts +10 -0
- package/dist/sandbox/prompt.d.ts.map +1 -0
- package/dist/sandbox/prompt.js +434 -0
- package/dist/sandbox/prompt.js.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +59 -0
- package/dist/server.js.map +1 -0
- package/dist/terminal/index.d.ts +5 -0
- package/dist/terminal/index.d.ts.map +1 -0
- package/dist/terminal/index.js +3 -0
- package/dist/terminal/index.js.map +1 -0
- package/dist/terminal/manager.d.ts +153 -0
- package/dist/terminal/manager.d.ts.map +1 -0
- package/dist/terminal/manager.js +276 -0
- package/dist/terminal/manager.js.map +1 -0
- package/dist/terminal/session.d.ts +137 -0
- package/dist/terminal/session.d.ts.map +1 -0
- package/dist/terminal/session.js +752 -0
- package/dist/terminal/session.js.map +1 -0
- package/dist/tools/definitions.d.ts +18 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +114 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/getContent.d.ts +32 -0
- package/dist/tools/getContent.d.ts.map +1 -0
- package/dist/tools/getContent.js +38 -0
- package/dist/tools/getContent.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +49 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/screenshot.d.ts +20 -0
- package/dist/tools/screenshot.d.ts.map +1 -0
- package/dist/tools/screenshot.js +28 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/sendKey.d.ts +31 -0
- package/dist/tools/sendKey.d.ts.map +1 -0
- package/dist/tools/sendKey.js +38 -0
- package/dist/tools/sendKey.js.map +1 -0
- package/dist/tools/startRecording.d.ts +68 -0
- package/dist/tools/startRecording.d.ts.map +1 -0
- package/dist/tools/startRecording.js +111 -0
- package/dist/tools/startRecording.js.map +1 -0
- package/dist/tools/stopRecording.d.ts +31 -0
- package/dist/tools/stopRecording.d.ts.map +1 -0
- package/dist/tools/stopRecording.js +76 -0
- package/dist/tools/stopRecording.js.map +1 -0
- package/dist/tools/type.d.ts +31 -0
- package/dist/tools/type.d.ts.map +1 -0
- package/dist/tools/type.js +31 -0
- package/dist/tools/type.js.map +1 -0
- package/dist/transport/gui-protocol.d.ts +163 -0
- package/dist/transport/gui-protocol.d.ts.map +1 -0
- package/dist/transport/gui-protocol.js +68 -0
- package/dist/transport/gui-protocol.js.map +1 -0
- package/dist/transport/gui-stream.d.ts +139 -0
- package/dist/transport/gui-stream.d.ts.map +1 -0
- package/dist/transport/gui-stream.js +440 -0
- package/dist/transport/gui-stream.js.map +1 -0
- package/dist/transport/index.d.ts +6 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +6 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/socket.d.ts +46 -0
- package/dist/transport/socket.d.ts.map +1 -0
- package/dist/transport/socket.js +310 -0
- package/dist/transport/socket.js.map +1 -0
- package/dist/types/mcp-client-info.d.ts +226 -0
- package/dist/types/mcp-client-info.d.ts.map +1 -0
- package/dist/types/mcp-client-info.js +62 -0
- package/dist/types/mcp-client-info.js.map +1 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +84 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/utils/env.d.ts +17 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +35 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/keys.d.ts +16 -0
- package/dist/utils/keys.d.ts.map +1 -0
- package/dist/utils/keys.js +155 -0
- package/dist/utils/keys.js.map +1 -0
- package/dist/utils/platform.d.ts +16 -0
- package/dist/utils/platform.d.ts.map +1 -0
- package/dist/utils/platform.js +41 -0
- package/dist/utils/platform.js.map +1 -0
- package/dist/utils/session-logger.d.ts +31 -0
- package/dist/utils/session-logger.d.ts.map +1 -0
- package/dist/utils/session-logger.js +125 -0
- package/dist/utils/session-logger.js.map +1 -0
- package/dist/utils/stats.d.ts +46 -0
- package/dist/utils/stats.d.ts.map +1 -0
- package/dist/utils/stats.js +89 -0
- package/dist/utils/stats.js.map +1 -0
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +9 -0
- package/dist/utils/version.js.map +1 -0
- package/logo.png +0 -0
- package/package.json +61 -0
- package/packages/desktop-electron/THIRD-PARTY-NOTICES +56 -0
- package/packages/desktop-electron/build/afterPack.cjs +147 -0
- package/packages/desktop-electron/package-lock.json +10071 -0
- package/packages/desktop-electron/package.json +170 -0
- package/packages/desktop-electron/resources/icons/mac/icon.icns +0 -0
- package/packages/desktop-electron/resources/icons/png/1024x1024.png +0 -0
- package/packages/desktop-electron/resources/icons/png/128x128.png +0 -0
- package/packages/desktop-electron/resources/icons/png/16x16.png +0 -0
- package/packages/desktop-electron/resources/icons/png/24x24.png +0 -0
- package/packages/desktop-electron/resources/icons/png/256x256.png +0 -0
- package/packages/desktop-electron/resources/icons/png/32x32.png +0 -0
- package/packages/desktop-electron/resources/icons/png/48x48.png +0 -0
- package/packages/desktop-electron/resources/icons/png/512x512.png +0 -0
- package/packages/desktop-electron/resources/icons/png/64x64.png +0 -0
- package/packages/desktop-electron/resources/icons/win/icon.ico +0 -0
- package/packages/desktop-electron/scripts/download-models.js +97 -0
- package/packages/desktop-electron/scripts/prepare-sandbox-bins.js +186 -0
- package/packages/desktop-electron/tests/main/ai-detection/additionalFunctions.test.ts +224 -0
- package/packages/desktop-electron/tests/main/ai-detection/checkOverridePrefix.test.ts +162 -0
- package/packages/desktop-electron/tests/main/ai-detection/classifyInput.test.ts +132 -0
- package/packages/desktop-electron/tests/main/ai-detection/detectTypos.test.ts +342 -0
- package/packages/desktop-electron/tests/main/ai-detection/fixtures/commands.ts +134 -0
- package/packages/desktop-electron/tests/main/ai-detection/fixtures/natural-language.ts +133 -0
- package/packages/desktop-electron/tests/main/ai-detection/fixtures/typos.ts +123 -0
- package/packages/desktop-electron/tests/main/ai-detection/hasValidSubcommand.test.ts +218 -0
- package/packages/desktop-electron/tests/main/ai-detection/isCommandNotFound.test.ts +117 -0
- package/packages/desktop-electron/tests/main/error-triage/buildTriagePrompt.test.ts +133 -0
- package/packages/desktop-electron/tests/main/error-triage/parseTriageResponse.test.ts +123 -0
- package/packages/desktop-electron/tests/main/terminal-bridge/battery-optimization.test.ts +243 -0
- package/packages/desktop-electron/tests/main/terminal-bridge/command-fast-track.test.ts +292 -0
- package/packages/desktop-electron/tests/main/terminal-bridge/default-cwd.test.ts +70 -0
- package/packages/desktop-electron/tests/setup.ts +274 -0
- package/packages/desktop-electron/tsconfig.json +18 -0
- package/packages/desktop-electron/tsconfig.main.json +20 -0
- package/packages/desktop-electron/vite.config.ts +19 -0
- package/packages/desktop-electron/vitest.config.ts +18 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
import * as pty from "node-pty";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import xtermHeadless from "@xterm/headless";
|
|
7
|
+
const { Terminal } = xtermHeadless;
|
|
8
|
+
import { getDefaultShell } from "../utils/platform.js";
|
|
9
|
+
import { setEnv } from "../utils/env.js";
|
|
10
|
+
/**
|
|
11
|
+
* Get system locale like Hyper does.
|
|
12
|
+
* On macOS, reads from defaults. On Linux, checks environment.
|
|
13
|
+
*/
|
|
14
|
+
function getSystemLocale() {
|
|
15
|
+
// Check if already set to UTF-8
|
|
16
|
+
const lang = process.env.LANG || "";
|
|
17
|
+
if (lang.toLowerCase().includes("utf-8") || lang.toLowerCase().includes("utf8")) {
|
|
18
|
+
return lang;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
if (process.platform === "darwin") {
|
|
22
|
+
// macOS: read from system preferences
|
|
23
|
+
const output = execSync("defaults read -g AppleLocale 2>/dev/null", {
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
timeout: 1000,
|
|
26
|
+
}).trim();
|
|
27
|
+
if (output) {
|
|
28
|
+
// Convert format: en_US -> en_US.UTF-8
|
|
29
|
+
return `${output.replace(/-/g, "_")}.UTF-8`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Fall through to default
|
|
35
|
+
}
|
|
36
|
+
// Default to en_US.UTF-8
|
|
37
|
+
return "en_US.UTF-8";
|
|
38
|
+
}
|
|
39
|
+
// Custom prompt indicator for brosh (nf-md-palm_tree from Nerd Fonts, U+F1055)
|
|
40
|
+
const PROMPT_INDICATOR = "\uDB84\uDC55";
|
|
41
|
+
/**
|
|
42
|
+
* Terminal session that combines node-pty with xterm.js headless
|
|
43
|
+
* for full terminal emulation
|
|
44
|
+
*/
|
|
45
|
+
export class TerminalSession {
|
|
46
|
+
ptyProcess;
|
|
47
|
+
terminal;
|
|
48
|
+
disposed = false;
|
|
49
|
+
dataListeners = [];
|
|
50
|
+
exitListeners = [];
|
|
51
|
+
resizeListeners = [];
|
|
52
|
+
rcFile = null;
|
|
53
|
+
zdotdir = null;
|
|
54
|
+
/**
|
|
55
|
+
* Private constructor - use TerminalSession.create() instead
|
|
56
|
+
*/
|
|
57
|
+
constructor() { }
|
|
58
|
+
/**
|
|
59
|
+
* Factory method to create a TerminalSession
|
|
60
|
+
* Use this instead of the constructor to support async sandbox initialization
|
|
61
|
+
*/
|
|
62
|
+
static async create(options = {}) {
|
|
63
|
+
const session = new TerminalSession();
|
|
64
|
+
await session.initialize(options);
|
|
65
|
+
return session;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Set up shell-specific prompt customization
|
|
69
|
+
* Returns args to pass to shell and env modifications
|
|
70
|
+
*/
|
|
71
|
+
setupShellPrompt(shellName, extraEnv, startupBanner) {
|
|
72
|
+
const env = {
|
|
73
|
+
...extraEnv,
|
|
74
|
+
};
|
|
75
|
+
setEnv(env, 'BROSH', 'TERMINAL_MCP', '1');
|
|
76
|
+
// Escape banner for use in shell scripts
|
|
77
|
+
const escapeBannerForShell = (banner) => {
|
|
78
|
+
// Escape single quotes and backslashes for shell
|
|
79
|
+
return banner.replace(/'/g, "'\\''");
|
|
80
|
+
};
|
|
81
|
+
if (shellName === "bash" || shellName === "sh") {
|
|
82
|
+
// Create temp rcfile that sources user's .bashrc then sets our prompt
|
|
83
|
+
const homeDir = os.homedir();
|
|
84
|
+
const bannerCmd = startupBanner ? `printf '%s\\n' '${escapeBannerForShell(startupBanner)}'` : "";
|
|
85
|
+
const bashrcContent = `
|
|
86
|
+
# Source user's bashrc if it exists
|
|
87
|
+
[ -f "${homeDir}/.bashrc" ] && source "${homeDir}/.bashrc"
|
|
88
|
+
|
|
89
|
+
# Function to get MCP status for prompt
|
|
90
|
+
__brosh_mcp_status() {
|
|
91
|
+
if [ -f /tmp/brosh-mcp-status ]; then
|
|
92
|
+
local __brosh_st=$(cat /tmp/brosh-mcp-status 2>/dev/null)
|
|
93
|
+
if [ "$__brosh_st" = "enabled" ]; then
|
|
94
|
+
printf '\\033[32m(MCP Enabled)\\033[0m'
|
|
95
|
+
else
|
|
96
|
+
printf '\\033[31m(MCP Disabled)\\033[0m'
|
|
97
|
+
fi
|
|
98
|
+
else
|
|
99
|
+
printf '\\033[31m(MCP Disabled)\\033[0m'
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# Set brosh prompt and clear PROMPT_COMMAND so prompt managers
|
|
104
|
+
# (Starship, bash-it, oh-my-bash, etc.) can't override PS1
|
|
105
|
+
PROMPT_COMMAND=""
|
|
106
|
+
PS1="\\[\\033[33m\\]${PROMPT_INDICATOR}\\[\\033[0m\\] \$(__brosh_mcp_status)> "
|
|
107
|
+
# Print startup banner
|
|
108
|
+
${bannerCmd}
|
|
109
|
+
`;
|
|
110
|
+
this.rcFile = path.join(os.tmpdir(), `brosh-bashrc-${process.pid}`);
|
|
111
|
+
fs.writeFileSync(this.rcFile, bashrcContent);
|
|
112
|
+
return { args: ["--rcfile", this.rcFile], env };
|
|
113
|
+
}
|
|
114
|
+
if (shellName === "zsh") {
|
|
115
|
+
// Create temp ZDOTDIR with .zshrc that sources user's config then sets prompt
|
|
116
|
+
const homeDir = os.homedir();
|
|
117
|
+
this.zdotdir = path.join(os.tmpdir(), `brosh-zsh-${process.pid}`);
|
|
118
|
+
fs.mkdirSync(this.zdotdir, { recursive: true });
|
|
119
|
+
const bannerCmd = startupBanner ? `printf '%s\\n' '${escapeBannerForShell(startupBanner)}'` : "";
|
|
120
|
+
const zshrcContent = `
|
|
121
|
+
# Disable p10k instant prompt before sourcing user config
|
|
122
|
+
# (our banner output during init would trigger warnings otherwise)
|
|
123
|
+
typeset -g POWERLEVEL9K_INSTANT_PROMPT=off
|
|
124
|
+
|
|
125
|
+
# Reset ZDOTDIR so nested zsh uses normal config
|
|
126
|
+
export ZDOTDIR="${homeDir}"
|
|
127
|
+
# Source user's zshrc if it exists
|
|
128
|
+
[ -f "${homeDir}/.zshrc" ] && source "${homeDir}/.zshrc"
|
|
129
|
+
|
|
130
|
+
# Neuter prompt managers that use precmd hooks to override PROMPT.
|
|
131
|
+
# We source user config for aliases/PATH/functions, but brosh uses its own prompt.
|
|
132
|
+
# Covers: Powerlevel10k, Starship, Pure, Spaceship, Oh My Zsh themes
|
|
133
|
+
(( \${+functions[_p9k_precmd]} )) && _p9k_precmd() { }
|
|
134
|
+
(( \${+functions[_p9k_preexec]} )) && _p9k_preexec() { }
|
|
135
|
+
(( \${+functions[starship_precmd]} )) && starship_precmd() { }
|
|
136
|
+
(( \${+functions[starship_preexec]} )) && starship_preexec() { }
|
|
137
|
+
(( \${+functions[prompt_pure_precmd]} )) && prompt_pure_precmd() { }
|
|
138
|
+
(( \${+functions[spaceship_precmd]} )) && spaceship_precmd() { }
|
|
139
|
+
|
|
140
|
+
# Clear all precmd hooks except ours to prevent any remaining prompt overrides
|
|
141
|
+
precmd_functions=()
|
|
142
|
+
|
|
143
|
+
# Function to get MCP status for prompt
|
|
144
|
+
__brosh_mcp_status() {
|
|
145
|
+
if [[ -f /tmp/brosh-mcp-status ]]; then
|
|
146
|
+
local __brosh_st=$(<"/tmp/brosh-mcp-status" 2>/dev/null)
|
|
147
|
+
if [[ "$__brosh_st" = "enabled" ]]; then
|
|
148
|
+
print -n '%F{green}(MCP Enabled)%f'
|
|
149
|
+
else
|
|
150
|
+
print -n '%F{red}(MCP Disabled)%f'
|
|
151
|
+
fi
|
|
152
|
+
else
|
|
153
|
+
print -n '%F{red}(MCP Disabled)%f'
|
|
154
|
+
fi
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Enable prompt substitution for dynamic status
|
|
158
|
+
setopt PROMPT_SUBST
|
|
159
|
+
|
|
160
|
+
# Set brosh prompt with dynamic MCP status
|
|
161
|
+
PROMPT='%F{yellow}${PROMPT_INDICATOR}%f $(__brosh_mcp_status)> '
|
|
162
|
+
RPROMPT=""
|
|
163
|
+
# Print startup banner
|
|
164
|
+
${bannerCmd}
|
|
165
|
+
`;
|
|
166
|
+
fs.writeFileSync(path.join(this.zdotdir, ".zshrc"), zshrcContent);
|
|
167
|
+
env.ZDOTDIR = this.zdotdir;
|
|
168
|
+
return { args: [], env };
|
|
169
|
+
}
|
|
170
|
+
// PowerShell (pwsh is PowerShell Core, powershell is Windows PowerShell)
|
|
171
|
+
if (shellName === "powershell" ||
|
|
172
|
+
shellName === "powershell.exe" ||
|
|
173
|
+
shellName === "pwsh" ||
|
|
174
|
+
shellName === "pwsh.exe") {
|
|
175
|
+
setEnv(env, 'BROSH_PROMPT', 'TERMINAL_MCP_PROMPT', '1');
|
|
176
|
+
return { args: ["-NoLogo"], env };
|
|
177
|
+
}
|
|
178
|
+
// Windows cmd.exe
|
|
179
|
+
if (shellName === "cmd" || shellName === "cmd.exe") {
|
|
180
|
+
env.PROMPT = `\x1b[33m${PROMPT_INDICATOR}\x1b[0m $P$G`;
|
|
181
|
+
return { args: [], env };
|
|
182
|
+
}
|
|
183
|
+
// For other shells, just set env vars and hope for the best
|
|
184
|
+
env.PS1 = `\x1b[33m${PROMPT_INDICATOR}\x1b[0m $ `;
|
|
185
|
+
return { args: [], env };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get a list of available UTF-8 locales on the system.
|
|
189
|
+
* Returns the best one to use, preferring the user's existing locale if valid.
|
|
190
|
+
*/
|
|
191
|
+
getAvailableUtf8Locale() {
|
|
192
|
+
const isUtf8 = (locale) => locale.toLowerCase().includes("utf-8") || locale.toLowerCase().includes("utf8");
|
|
193
|
+
// Check if user already has a UTF-8 locale set
|
|
194
|
+
const userLang = process.env.LANG || "";
|
|
195
|
+
const userLcAll = process.env.LC_ALL || "";
|
|
196
|
+
if (isUtf8(userLcAll))
|
|
197
|
+
return userLcAll;
|
|
198
|
+
if (isUtf8(userLang))
|
|
199
|
+
return userLang;
|
|
200
|
+
// Try to detect available locales
|
|
201
|
+
try {
|
|
202
|
+
const { execSync } = require("child_process");
|
|
203
|
+
const localeOutput = execSync("locale -a 2>/dev/null", {
|
|
204
|
+
encoding: "utf8",
|
|
205
|
+
timeout: 1000,
|
|
206
|
+
});
|
|
207
|
+
const locales = localeOutput.split("\n").filter((l) => isUtf8(l));
|
|
208
|
+
// Prefer C.UTF-8 as it's most portable (available on most Linux systems)
|
|
209
|
+
if (locales.includes("C.UTF-8"))
|
|
210
|
+
return "C.UTF-8";
|
|
211
|
+
if (locales.includes("C.utf8"))
|
|
212
|
+
return "C.utf8";
|
|
213
|
+
// Then try POSIX UTF-8 variants
|
|
214
|
+
if (locales.includes("POSIX.UTF-8"))
|
|
215
|
+
return "POSIX.UTF-8";
|
|
216
|
+
// Then try en_US.UTF-8 variants
|
|
217
|
+
const enUs = locales.find((l) => l.startsWith("en_US") && isUtf8(l));
|
|
218
|
+
if (enUs)
|
|
219
|
+
return enUs;
|
|
220
|
+
// Use any available UTF-8 locale
|
|
221
|
+
if (locales.length > 0)
|
|
222
|
+
return locales[0];
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// locale command failed, use platform-specific defaults
|
|
226
|
+
}
|
|
227
|
+
// Platform-specific fallbacks
|
|
228
|
+
if (process.platform === "darwin") {
|
|
229
|
+
// macOS always has en_US.UTF-8
|
|
230
|
+
return "en_US.UTF-8";
|
|
231
|
+
}
|
|
232
|
+
// Linux/other: C.UTF-8 is the most portable
|
|
233
|
+
return "C.UTF-8";
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Ensure proper UTF-8 locale settings for the terminal.
|
|
237
|
+
* Electron apps launched from Finder on macOS don't inherit shell locale settings,
|
|
238
|
+
* which breaks programs like mosh that require UTF-8.
|
|
239
|
+
*/
|
|
240
|
+
ensureUtf8Locale(env) {
|
|
241
|
+
const result = { ...env };
|
|
242
|
+
const isUtf8 = (locale) => locale.toLowerCase().includes("utf-8") || locale.toLowerCase().includes("utf8");
|
|
243
|
+
// Check current locale settings
|
|
244
|
+
const lang = process.env.LANG || "";
|
|
245
|
+
const lcCtype = process.env.LC_CTYPE || "";
|
|
246
|
+
const lcAll = process.env.LC_ALL || "";
|
|
247
|
+
// If already UTF-8, don't change anything
|
|
248
|
+
if (isUtf8(lcAll) || (isUtf8(lang) && isUtf8(lcCtype))) {
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
// Get a valid UTF-8 locale
|
|
252
|
+
const targetLocale = this.getAvailableUtf8Locale();
|
|
253
|
+
// Set LANG if not UTF-8
|
|
254
|
+
if (!isUtf8(lang)) {
|
|
255
|
+
result.LANG = targetLocale;
|
|
256
|
+
}
|
|
257
|
+
// Set LC_CTYPE specifically for character encoding (most important for mosh)
|
|
258
|
+
if (!isUtf8(lcCtype)) {
|
|
259
|
+
result.LC_CTYPE = targetLocale;
|
|
260
|
+
}
|
|
261
|
+
// Clear LC_ALL if it's set to a non-UTF-8 value (it overrides everything)
|
|
262
|
+
if (lcAll && !isUtf8(lcAll)) {
|
|
263
|
+
result.LC_ALL = "";
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Initialize the terminal session
|
|
269
|
+
* This is called by the create() factory method
|
|
270
|
+
*/
|
|
271
|
+
async initialize(options) {
|
|
272
|
+
const cols = options.cols ?? 120;
|
|
273
|
+
const rows = options.rows ?? 40;
|
|
274
|
+
const shell = options.shell ?? getDefaultShell();
|
|
275
|
+
// Create headless terminal emulator
|
|
276
|
+
this.terminal = new Terminal({
|
|
277
|
+
cols,
|
|
278
|
+
rows,
|
|
279
|
+
scrollback: 1000,
|
|
280
|
+
allowProposedApi: true,
|
|
281
|
+
});
|
|
282
|
+
// Determine shell type and set up custom prompt (unless nativeShell is enabled)
|
|
283
|
+
const shellName = path.basename(shell);
|
|
284
|
+
let args = [];
|
|
285
|
+
let env = { ...options.env };
|
|
286
|
+
console.log(`[session] shell=${shell}, shellName=${shellName}, nativeShell=${options.nativeShell}`);
|
|
287
|
+
if (options.nativeShell) {
|
|
288
|
+
// Use native shell without customization - just set BROSH env var
|
|
289
|
+
// Spawn as login shell so user's profile is sourced (sets up PATH, aliases, etc.)
|
|
290
|
+
setEnv(env, 'BROSH', 'TERMINAL_MCP', '1');
|
|
291
|
+
if (shellName === "bash" || shellName === "sh") {
|
|
292
|
+
// Bash: use --rcfile with a temp file that sources the user's profile,
|
|
293
|
+
// injects OSC 7 (cwd reporting) and OSC 133 (command marks for error detection)
|
|
294
|
+
const homeDir = os.homedir();
|
|
295
|
+
const bashRcContent = `
|
|
296
|
+
# Source user's login profile for PATH, aliases, etc.
|
|
297
|
+
[ -f "${homeDir}/.bash_profile" ] && source "${homeDir}/.bash_profile" || {
|
|
298
|
+
[ -f "${homeDir}/.bash_login" ] && source "${homeDir}/.bash_login" || {
|
|
299
|
+
[ -f "${homeDir}/.profile" ] && source "${homeDir}/.profile"
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
# Source .bashrc if not already sourced by the profile above
|
|
303
|
+
[ -f "${homeDir}/.bashrc" ] && source "${homeDir}/.bashrc"
|
|
304
|
+
|
|
305
|
+
# Emit OSC 7 (current working directory) on every prompt
|
|
306
|
+
# This lets the terminal track cwd changes for status bar badges
|
|
307
|
+
__brosh_osc7() {
|
|
308
|
+
printf '\\e]7;file://%s%s\\e\\\\' "$HOSTNAME" "$PWD"
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# OSC 133 shell integration (command marks for error detection)
|
|
312
|
+
# A = prompt start, C = output start, D;exitcode = command finished
|
|
313
|
+
__brosh_cmd_executed=""
|
|
314
|
+
__brosh_precmd() {
|
|
315
|
+
local __brosh_exit=$?
|
|
316
|
+
if [[ -n "$__brosh_cmd_executed" ]]; then
|
|
317
|
+
printf '\\e]133;D;%d\\a' "$__brosh_exit"
|
|
318
|
+
__brosh_cmd_executed=""
|
|
319
|
+
fi
|
|
320
|
+
printf '\\e]133;A\\a'
|
|
321
|
+
}
|
|
322
|
+
PROMPT_COMMAND="__brosh_precmd;__brosh_osc7\${PROMPT_COMMAND:+;\\$PROMPT_COMMAND}"
|
|
323
|
+
|
|
324
|
+
# DEBUG trap for preexec (marks output start when command begins executing)
|
|
325
|
+
trap '
|
|
326
|
+
if [[ -z "$__brosh_cmd_executed" && "$BASH_COMMAND" != "__brosh_precmd" && "$BASH_COMMAND" != "__brosh_osc7" ]]; then
|
|
327
|
+
__brosh_cmd_executed=1
|
|
328
|
+
printf '"'"'\\e]133;C\\a'"'"'
|
|
329
|
+
fi
|
|
330
|
+
' DEBUG
|
|
331
|
+
`;
|
|
332
|
+
this.rcFile = path.join(os.tmpdir(), `brosh-bashrc-${process.pid}-${Date.now()}`);
|
|
333
|
+
fs.writeFileSync(this.rcFile, bashRcContent);
|
|
334
|
+
args = ["--rcfile", this.rcFile];
|
|
335
|
+
}
|
|
336
|
+
else if (shellName === "zsh") {
|
|
337
|
+
// Zsh: create ZDOTDIR wrapper that sources user's config and adds
|
|
338
|
+
// OSC 133 shell integration hooks (non-destructive via add-zsh-hook)
|
|
339
|
+
const userZdotdir = process.env.ZDOTDIR || os.homedir();
|
|
340
|
+
this.zdotdir = path.join(os.tmpdir(), `brosh-zsh-${process.pid}-${Date.now()}`);
|
|
341
|
+
fs.mkdirSync(this.zdotdir, { recursive: true });
|
|
342
|
+
console.log(`[session] Created ZDOTDIR: ${this.zdotdir}, user ZDOTDIR: ${userZdotdir}`);
|
|
343
|
+
// .zshenv - runs for all zsh invocations (earliest rc file)
|
|
344
|
+
// Set HISTFILE here before zsh initializes history. Since ZDOTDIR is a
|
|
345
|
+
// temp dir, zsh's default HISTFILE ($ZDOTDIR/.zsh_history) would point
|
|
346
|
+
// to a nonexistent file. Must be set before user's .zshenv in case they
|
|
347
|
+
// rely on the default.
|
|
348
|
+
fs.writeFileSync(path.join(this.zdotdir, ".zshenv"), `typeset -g __BROSH_WRAPPER_ZDOTDIR="$ZDOTDIR"\n` +
|
|
349
|
+
`export ZDOTDIR="${userZdotdir}"\n` +
|
|
350
|
+
`HISTFILE="${userZdotdir}/.zsh_history"\n` +
|
|
351
|
+
`[[ -f "${userZdotdir}/.zshenv" ]] && source "${userZdotdir}/.zshenv"\n` +
|
|
352
|
+
`export ZDOTDIR="$__BROSH_WRAPPER_ZDOTDIR"\n` +
|
|
353
|
+
`unset __BROSH_WRAPPER_ZDOTDIR\n` +
|
|
354
|
+
`[[ -z "$HISTFILE" ]] && HISTFILE="${userZdotdir}/.zsh_history"\n` +
|
|
355
|
+
`[[ "$HISTFILE" == "$ZDOTDIR/.zsh_history" ]] && HISTFILE="${userZdotdir}/.zsh_history"\n`);
|
|
356
|
+
// .zprofile - runs for login shells (before .zshrc)
|
|
357
|
+
fs.writeFileSync(path.join(this.zdotdir, ".zprofile"), `typeset -g __BROSH_WRAPPER_ZDOTDIR="$ZDOTDIR"\n` +
|
|
358
|
+
`export ZDOTDIR="${userZdotdir}"\n` +
|
|
359
|
+
`[[ -f "${userZdotdir}/.zprofile" ]] && source "${userZdotdir}/.zprofile"\n` +
|
|
360
|
+
`export ZDOTDIR="$__BROSH_WRAPPER_ZDOTDIR"\n` +
|
|
361
|
+
`unset __BROSH_WRAPPER_ZDOTDIR\n` +
|
|
362
|
+
`[[ -z "$HISTFILE" ]] && HISTFILE="${userZdotdir}/.zsh_history"\n` +
|
|
363
|
+
`[[ "$HISTFILE" == "$ZDOTDIR/.zsh_history" ]] && HISTFILE="${userZdotdir}/.zsh_history"\n`);
|
|
364
|
+
// .zshrc - runs for interactive shells
|
|
365
|
+
const zshrcContent = `
|
|
366
|
+
# Keep wrapper ZDOTDIR so brosh startup files still run.
|
|
367
|
+
typeset -g __BROSH_WRAPPER_ZDOTDIR="$ZDOTDIR"
|
|
368
|
+
|
|
369
|
+
# Make user config see the real ZDOTDIR.
|
|
370
|
+
# Many zsh configs derive HISTFILE from ZDOTDIR.
|
|
371
|
+
export ZDOTDIR="${userZdotdir}"
|
|
372
|
+
[[ -f "${userZdotdir}/.zshrc" ]] && source "${userZdotdir}/.zshrc"
|
|
373
|
+
|
|
374
|
+
# Restore wrapper ZDOTDIR for the remainder of startup.
|
|
375
|
+
export ZDOTDIR="$__BROSH_WRAPPER_ZDOTDIR"
|
|
376
|
+
unset __BROSH_WRAPPER_ZDOTDIR
|
|
377
|
+
|
|
378
|
+
# If user config left HISTFILE empty or pointing to wrapper temp ZDOTDIR,
|
|
379
|
+
# reset it to the user's default history file.
|
|
380
|
+
[[ -z "$HISTFILE" ]] && HISTFILE="${userZdotdir}/.zsh_history"
|
|
381
|
+
[[ "$HISTFILE" == "$ZDOTDIR/.zsh_history" ]] && HISTFILE="${userZdotdir}/.zsh_history"
|
|
382
|
+
|
|
383
|
+
# HISTFILE is set in .zshenv (before zsh initializes history).
|
|
384
|
+
# Set size defaults here; user's .zshrc can override.
|
|
385
|
+
: \${HISTSIZE:=50000}
|
|
386
|
+
: \${SAVEHIST:=10000}
|
|
387
|
+
|
|
388
|
+
# OSC 133 shell integration (command marks for error detection)
|
|
389
|
+
# A = prompt start, C = output start, D;exitcode = command finished
|
|
390
|
+
# Uses add-zsh-hook so it doesn't interfere with user's hooks
|
|
391
|
+
__brosh_cmd_executed=""
|
|
392
|
+
__brosh_precmd() {
|
|
393
|
+
local exit_code=$?
|
|
394
|
+
if [[ -n "$__brosh_cmd_executed" ]]; then
|
|
395
|
+
printf '\\e]133;D;%d\\a' "$exit_code"
|
|
396
|
+
fi
|
|
397
|
+
__brosh_cmd_executed=""
|
|
398
|
+
printf '\\e]133;A\\a'
|
|
399
|
+
}
|
|
400
|
+
__brosh_preexec() {
|
|
401
|
+
__brosh_cmd_executed=1
|
|
402
|
+
printf '\\e]133;C\\a'
|
|
403
|
+
}
|
|
404
|
+
autoload -Uz add-zsh-hook
|
|
405
|
+
add-zsh-hook precmd __brosh_precmd
|
|
406
|
+
add-zsh-hook preexec __brosh_preexec
|
|
407
|
+
`;
|
|
408
|
+
fs.writeFileSync(path.join(this.zdotdir, ".zshrc"), zshrcContent);
|
|
409
|
+
// .zlogin - runs for login shells (after .zshrc) — last rc file
|
|
410
|
+
// Force-read history here, after restoring the user's ZDOTDIR context.
|
|
411
|
+
fs.writeFileSync(path.join(this.zdotdir, ".zlogin"), `typeset -g __BROSH_WRAPPER_ZDOTDIR="$ZDOTDIR"\n` +
|
|
412
|
+
`export ZDOTDIR="${userZdotdir}"\n` +
|
|
413
|
+
`[[ -f "${userZdotdir}/.zlogin" ]] && source "${userZdotdir}/.zlogin"\n` +
|
|
414
|
+
`export ZDOTDIR="$__BROSH_WRAPPER_ZDOTDIR"\n` +
|
|
415
|
+
`unset __BROSH_WRAPPER_ZDOTDIR\n` +
|
|
416
|
+
`[[ -z "$HISTFILE" ]] && HISTFILE="${userZdotdir}/.zsh_history"\n` +
|
|
417
|
+
`[[ "$HISTFILE" == "$ZDOTDIR/.zsh_history" ]] && HISTFILE="${userZdotdir}/.zsh_history"\n` +
|
|
418
|
+
`[[ -s "\$HISTFILE" ]] && fc -R "\$HISTFILE"\n` +
|
|
419
|
+
// Keep runtime ZDOTDIR on the user's path so prompt hooks/plugins
|
|
420
|
+
// that derive HISTFILE from ZDOTDIR don't switch back to wrapper temp dir.
|
|
421
|
+
`export ZDOTDIR="${userZdotdir}"\n`);
|
|
422
|
+
env.ZDOTDIR = this.zdotdir;
|
|
423
|
+
args = ["--login"];
|
|
424
|
+
}
|
|
425
|
+
// Set LANG for local UTF-8 support (like iTerm2's "Set locale environment
|
|
426
|
+
// variables automatically"). By default we only set LANG, not LC_CTYPE,
|
|
427
|
+
// because SSH's SendEnv forwards LC_* variables and remote servers may not
|
|
428
|
+
// have the same locales installed. The system derives LC_CTYPE from LANG locally.
|
|
429
|
+
const systemLocale = getSystemLocale();
|
|
430
|
+
env.LANG = systemLocale;
|
|
431
|
+
// Optionally set LC_CTYPE too (may cause SSH issues with remote servers)
|
|
432
|
+
if (options.setLocaleEnv) {
|
|
433
|
+
env.LC_CTYPE = systemLocale;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// Set up brosh custom prompt
|
|
438
|
+
const promptSetup = this.setupShellPrompt(shellName, options.env, options.startupBanner);
|
|
439
|
+
args = promptSetup.args;
|
|
440
|
+
env = promptSetup.env;
|
|
441
|
+
// Ensure UTF-8 locale for MCP mode (headless operation)
|
|
442
|
+
env = this.ensureUtf8Locale(env);
|
|
443
|
+
}
|
|
444
|
+
// Determine spawn command - may be wrapped by sandbox
|
|
445
|
+
let spawnCmd = shell;
|
|
446
|
+
let spawnArgs = args;
|
|
447
|
+
if (options.sandboxController?.isActive()) {
|
|
448
|
+
const wrapped = await options.sandboxController.wrapShellCommand(shell, args);
|
|
449
|
+
spawnCmd = wrapped.cmd;
|
|
450
|
+
spawnArgs = wrapped.args;
|
|
451
|
+
if (process.env.DEBUG_SANDBOX) {
|
|
452
|
+
console.error("[sandbox-debug] Spawn command:", spawnCmd);
|
|
453
|
+
console.error("[sandbox-debug] Spawn args:", spawnArgs.join(" "));
|
|
454
|
+
console.error("[sandbox-debug] CWD:", options.cwd ?? process.cwd());
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// Build the final environment for the PTY process
|
|
458
|
+
// By default, filter out LC_* variables from inherited environment to avoid
|
|
459
|
+
// SSH forwarding issues (SSH's SendEnv LC_* forwards these to remote servers
|
|
460
|
+
// that may not have the same locales). We set LANG which is sufficient for
|
|
461
|
+
// local UTF-8 support - the shell derives other locale settings from LANG.
|
|
462
|
+
const baseEnv = {};
|
|
463
|
+
const filterLcVars = options.nativeShell && !options.setLocaleEnv;
|
|
464
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
465
|
+
// Skip LC_* variables when filtering is enabled
|
|
466
|
+
if (filterLcVars && key.startsWith("LC_")) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
// Strip npm internal variables — these leak from Electron's parent process
|
|
470
|
+
// (npm run dev/start) and break tools like nvm that check npm_config_prefix
|
|
471
|
+
if (key.startsWith("npm_")) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
if (value !== undefined) {
|
|
475
|
+
baseEnv[key] = value;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Spawn PTY process
|
|
479
|
+
this.ptyProcess = pty.spawn(spawnCmd, spawnArgs, {
|
|
480
|
+
name: "xterm-256color",
|
|
481
|
+
cols,
|
|
482
|
+
rows,
|
|
483
|
+
cwd: options.cwd ?? process.cwd(),
|
|
484
|
+
env: { ...baseEnv, ...env },
|
|
485
|
+
});
|
|
486
|
+
// Pipe PTY output to terminal emulator and listeners
|
|
487
|
+
this.ptyProcess.onData((data) => {
|
|
488
|
+
if (!this.disposed) {
|
|
489
|
+
this.terminal.write(data);
|
|
490
|
+
// Notify all data listeners
|
|
491
|
+
for (const listener of this.dataListeners) {
|
|
492
|
+
listener(data);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
this.ptyProcess.onExit(({ exitCode }) => {
|
|
497
|
+
this.disposed = true;
|
|
498
|
+
for (const listener of this.exitListeners) {
|
|
499
|
+
listener(exitCode);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Subscribe to PTY output data
|
|
505
|
+
*/
|
|
506
|
+
onData(listener) {
|
|
507
|
+
this.dataListeners.push(listener);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Subscribe to PTY exit
|
|
511
|
+
*/
|
|
512
|
+
onExit(listener) {
|
|
513
|
+
this.exitListeners.push(listener);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Subscribe to terminal resize events
|
|
517
|
+
*/
|
|
518
|
+
onResize(listener) {
|
|
519
|
+
this.resizeListeners.push(listener);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Write data to the terminal (simulates typing)
|
|
523
|
+
*/
|
|
524
|
+
write(data) {
|
|
525
|
+
if (this.disposed) {
|
|
526
|
+
throw new Error("Terminal session has been disposed");
|
|
527
|
+
}
|
|
528
|
+
this.ptyProcess.write(data);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Get the current terminal buffer content as plain text
|
|
532
|
+
*/
|
|
533
|
+
getContent() {
|
|
534
|
+
if (this.disposed) {
|
|
535
|
+
throw new Error("Terminal session has been disposed");
|
|
536
|
+
}
|
|
537
|
+
const buffer = this.terminal.buffer.active;
|
|
538
|
+
const lines = [];
|
|
539
|
+
// Get all lines from the buffer (including scrollback)
|
|
540
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
541
|
+
const line = buffer.getLine(i);
|
|
542
|
+
if (line) {
|
|
543
|
+
lines.push(line.translateToString(true));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// Trim trailing empty lines
|
|
547
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
548
|
+
lines.pop();
|
|
549
|
+
}
|
|
550
|
+
return lines.join("\n");
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Get only the visible viewport content
|
|
554
|
+
*/
|
|
555
|
+
getVisibleContent() {
|
|
556
|
+
if (this.disposed) {
|
|
557
|
+
throw new Error("Terminal session has been disposed");
|
|
558
|
+
}
|
|
559
|
+
const buffer = this.terminal.buffer.active;
|
|
560
|
+
const lines = [];
|
|
561
|
+
const baseY = buffer.baseY;
|
|
562
|
+
for (let i = 0; i < this.terminal.rows; i++) {
|
|
563
|
+
const line = buffer.getLine(baseY + i);
|
|
564
|
+
if (line) {
|
|
565
|
+
lines.push(line.translateToString(true));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return lines.join("\n");
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Take a screenshot of the terminal state
|
|
572
|
+
*/
|
|
573
|
+
takeScreenshot() {
|
|
574
|
+
if (this.disposed) {
|
|
575
|
+
throw new Error("Terminal session has been disposed");
|
|
576
|
+
}
|
|
577
|
+
const buffer = this.terminal.buffer.active;
|
|
578
|
+
return {
|
|
579
|
+
content: this.getVisibleContent(),
|
|
580
|
+
cursor: {
|
|
581
|
+
x: buffer.cursorX,
|
|
582
|
+
y: buffer.cursorY,
|
|
583
|
+
},
|
|
584
|
+
dimensions: {
|
|
585
|
+
cols: this.terminal.cols,
|
|
586
|
+
rows: this.terminal.rows,
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Clear the terminal screen
|
|
592
|
+
*/
|
|
593
|
+
clear() {
|
|
594
|
+
if (this.disposed) {
|
|
595
|
+
throw new Error("Terminal session has been disposed");
|
|
596
|
+
}
|
|
597
|
+
this.terminal.clear();
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Resize the terminal
|
|
601
|
+
*/
|
|
602
|
+
resize(cols, rows) {
|
|
603
|
+
if (this.disposed) {
|
|
604
|
+
throw new Error("Terminal session has been disposed");
|
|
605
|
+
}
|
|
606
|
+
this.terminal.resize(cols, rows);
|
|
607
|
+
this.ptyProcess.resize(cols, rows);
|
|
608
|
+
// Notify all resize listeners
|
|
609
|
+
for (const listener of this.resizeListeners) {
|
|
610
|
+
listener(cols, rows);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Check if the session is still active
|
|
615
|
+
*/
|
|
616
|
+
isActive() {
|
|
617
|
+
return !this.disposed;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Get terminal dimensions
|
|
621
|
+
*/
|
|
622
|
+
getDimensions() {
|
|
623
|
+
return {
|
|
624
|
+
cols: this.terminal.cols,
|
|
625
|
+
rows: this.terminal.rows,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Get the current foreground process name
|
|
630
|
+
* On macOS/Linux this returns the actual process running in the terminal
|
|
631
|
+
*/
|
|
632
|
+
getProcess() {
|
|
633
|
+
if (this.disposed) {
|
|
634
|
+
return "shell";
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
// node-pty's process property returns the current foreground process
|
|
638
|
+
return this.ptyProcess.process || "shell";
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return "shell";
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Get the current working directory of the foreground process
|
|
646
|
+
* Uses process tree traversal to find the deepest child process
|
|
647
|
+
*/
|
|
648
|
+
getCwd() {
|
|
649
|
+
if (this.disposed) {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
const pid = this.ptyProcess.pid;
|
|
654
|
+
// Validate PID before using in shell command
|
|
655
|
+
if (!pid || typeof pid !== "number" || pid <= 0) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
if (process.platform === "darwin") {
|
|
659
|
+
// macOS: Find the foreground process by traversing the process tree
|
|
660
|
+
// When you run `bash` inside zsh, bash is a child of zsh
|
|
661
|
+
// We need to find the deepest child (the actual foreground process)
|
|
662
|
+
// and get its cwd
|
|
663
|
+
//
|
|
664
|
+
// Use pgrep to find child processes, then get the leaf (deepest child)
|
|
665
|
+
// This handles: zsh -> bash -> python, etc.
|
|
666
|
+
const findLeafPid = (parentPid) => {
|
|
667
|
+
try {
|
|
668
|
+
const children = execSync(`pgrep -P ${parentPid} 2>/dev/null`, {
|
|
669
|
+
encoding: "utf8",
|
|
670
|
+
timeout: 500,
|
|
671
|
+
}).trim();
|
|
672
|
+
if (children) {
|
|
673
|
+
// Take the first child and recurse
|
|
674
|
+
const childPid = parseInt(children.split("\n")[0], 10);
|
|
675
|
+
if (childPid > 0) {
|
|
676
|
+
return findLeafPid(childPid);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
// No children, this is the leaf
|
|
682
|
+
}
|
|
683
|
+
return parentPid;
|
|
684
|
+
};
|
|
685
|
+
const leafPid = findLeafPid(pid);
|
|
686
|
+
// Get cwd of the leaf process
|
|
687
|
+
const output = execSync(`lsof -a -p ${leafPid} -d cwd 2>/dev/null | awk 'NR==2 {print $NF}'`, {
|
|
688
|
+
encoding: "utf8",
|
|
689
|
+
timeout: 1000,
|
|
690
|
+
}).trim();
|
|
691
|
+
// Validate output looks like a path
|
|
692
|
+
if (output && output.startsWith("/")) {
|
|
693
|
+
return output;
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
else if (process.platform === "linux") {
|
|
698
|
+
// Linux: Find the foreground process similarly
|
|
699
|
+
const findLeafPid = (parentPid) => {
|
|
700
|
+
try {
|
|
701
|
+
const childrenPath = `/proc/${parentPid}/task/${parentPid}/children`;
|
|
702
|
+
const children = fs.readFileSync(childrenPath, "utf8").trim();
|
|
703
|
+
if (children) {
|
|
704
|
+
const childPid = parseInt(children.split(" ")[0], 10);
|
|
705
|
+
if (childPid > 0) {
|
|
706
|
+
return findLeafPid(childPid);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
// No children or can't read
|
|
712
|
+
}
|
|
713
|
+
return parentPid;
|
|
714
|
+
};
|
|
715
|
+
const leafPid = findLeafPid(pid);
|
|
716
|
+
return fs.readlinkSync(`/proc/${leafPid}/cwd`);
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Dispose of the terminal session
|
|
726
|
+
*/
|
|
727
|
+
dispose() {
|
|
728
|
+
if (!this.disposed) {
|
|
729
|
+
this.disposed = true;
|
|
730
|
+
this.ptyProcess.kill();
|
|
731
|
+
this.terminal.dispose();
|
|
732
|
+
// Clean up temp rc files
|
|
733
|
+
if (this.rcFile) {
|
|
734
|
+
try {
|
|
735
|
+
fs.unlinkSync(this.rcFile);
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// Ignore cleanup errors
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (this.zdotdir) {
|
|
742
|
+
try {
|
|
743
|
+
fs.rmSync(this.zdotdir, { recursive: true });
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
// Ignore cleanup errors
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
//# sourceMappingURL=session.js.map
|