tabminal 2.0.10 → 2.0.12
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/package.json +1 -1
- package/shell/tabminal-bashrc +17 -0
- package/shell/tabminal-hooks.bash +78 -0
- package/src/server.mjs +16 -0
- package/src/terminal-manager.mjs +52 -52
- package/src/terminal-session.mjs +29 -1
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
if [[ -n "${TABMINAL_SHELL_TOOLS_PATH:-}" ]]; then
|
|
2
|
+
export PATH="${TABMINAL_SHELL_TOOLS_PATH}:$PATH"
|
|
3
|
+
fi
|
|
4
|
+
|
|
5
|
+
if [[ -f ~/.bashrc ]]; then
|
|
6
|
+
source ~/.bashrc
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
if [[ -n "${TABMINAL_SHELL_TOOLS_PATH:-}" ]]; then
|
|
10
|
+
export PATH="${TABMINAL_SHELL_TOOLS_PATH}:$PATH"
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
if [[ -n "${TABMINAL_HOOKS_PATH:-}" ]] && [[ -f "$TABMINAL_HOOKS_PATH" ]]; then
|
|
14
|
+
source "$TABMINAL_HOOKS_PATH"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
TABMINAL_SHELL_READY=1
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
if [[ -z "${TABMINAL_SESSION_ID:-}" ]]; then
|
|
2
|
+
return 0
|
|
3
|
+
fi
|
|
4
|
+
|
|
5
|
+
if [[ -n "${TABMINAL_BASH_HOOKS_LOADED:-}" ]]; then
|
|
6
|
+
return 0
|
|
7
|
+
fi
|
|
8
|
+
|
|
9
|
+
TABMINAL_BASH_HOOKS_LOADED=1
|
|
10
|
+
|
|
11
|
+
_tabminal_bash_preexec() {
|
|
12
|
+
if [[ "${BASH_COMMAND:-}" == *"_tabminal_"* ]]; then
|
|
13
|
+
return
|
|
14
|
+
fi
|
|
15
|
+
if [[ "${BASH_COMMAND:-}" == "${PROMPT_COMMAND:-}" ]]; then
|
|
16
|
+
return
|
|
17
|
+
fi
|
|
18
|
+
_tabminal_last_command="$BASH_COMMAND"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_tabminal_bash_postexec() {
|
|
22
|
+
local exit_code="$?"
|
|
23
|
+
if [[ -n "${_tabminal_last_command:-}" ]]; then
|
|
24
|
+
local command_b64
|
|
25
|
+
command_b64=$(
|
|
26
|
+
echo -n "$_tabminal_last_command" | base64 | tr -d '\n'
|
|
27
|
+
)
|
|
28
|
+
printf '\x1b]1337;ExitCode=%s;CommandB64=%s\x07' \
|
|
29
|
+
"$exit_code" "$command_b64"
|
|
30
|
+
_tabminal_last_command=''
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_tabminal_apply_prompt_marker() {
|
|
35
|
+
local marker=$'\[\e]1337;TabminalPrompt\a\]'
|
|
36
|
+
if [[ "${PS1:-}" != *'TabminalPrompt'* ]]; then
|
|
37
|
+
PS1="${PS1}${marker}"
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_tabminal_prompt_contains() {
|
|
42
|
+
local needle="$1"
|
|
43
|
+
local current="${PROMPT_COMMAND:-}"
|
|
44
|
+
[[ "$current" == *"$needle"* ]]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_tabminal_install_prompt_command() {
|
|
48
|
+
if ! _tabminal_prompt_contains '_tabminal_bash_postexec'; then
|
|
49
|
+
if [[ -n "${PROMPT_COMMAND:-}" ]]; then
|
|
50
|
+
printf -v PROMPT_COMMAND '_tabminal_bash_postexec; %s' \
|
|
51
|
+
"$PROMPT_COMMAND"
|
|
52
|
+
else
|
|
53
|
+
PROMPT_COMMAND='_tabminal_bash_postexec'
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if ! _tabminal_prompt_contains '_tabminal_apply_prompt_marker'; then
|
|
58
|
+
if [[ -n "${PROMPT_COMMAND:-}" ]]; then
|
|
59
|
+
PROMPT_COMMAND="${PROMPT_COMMAND}; _tabminal_apply_prompt_marker"
|
|
60
|
+
else
|
|
61
|
+
PROMPT_COMMAND='_tabminal_apply_prompt_marker'
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_tabminal_install_tmux_wrapper() {
|
|
67
|
+
if ! command -v tmux >/dev/null 2>&1; then
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
tmux() {
|
|
72
|
+
command tmux -u "$@"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
trap '_tabminal_bash_preexec' DEBUG
|
|
77
|
+
_tabminal_install_prompt_command
|
|
78
|
+
_tabminal_install_tmux_wrapper
|
package/src/server.mjs
CHANGED
|
@@ -275,6 +275,14 @@ app.use(router.allowedMethods());
|
|
|
275
275
|
|
|
276
276
|
const httpServer = createServer(app.callback());
|
|
277
277
|
const wss = new WebSocketServer({ noServer: true, verifyClient });
|
|
278
|
+
const httpConnections = new Set();
|
|
279
|
+
|
|
280
|
+
httpServer.on('connection', (socket) => {
|
|
281
|
+
httpConnections.add(socket);
|
|
282
|
+
socket.on('close', () => {
|
|
283
|
+
httpConnections.delete(socket);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
278
286
|
|
|
279
287
|
httpServer.on('upgrade', (request, socket, head) => {
|
|
280
288
|
const url = new URL(request.url, `http://${request.headers.host}`);
|
|
@@ -370,6 +378,9 @@ function shutdown(signal) {
|
|
|
370
378
|
isShuttingDown = true;
|
|
371
379
|
console.log(`Shutting down (${signal})...`);
|
|
372
380
|
clearInterval(heartbeatInterval);
|
|
381
|
+
for (const socket of wss.clients) {
|
|
382
|
+
socket.terminate();
|
|
383
|
+
}
|
|
373
384
|
wss.close();
|
|
374
385
|
terminalManager.dispose();
|
|
375
386
|
|
|
@@ -382,6 +393,11 @@ function shutdown(signal) {
|
|
|
382
393
|
clearTimeout(forceExitTimer);
|
|
383
394
|
process.exit(0);
|
|
384
395
|
});
|
|
396
|
+
httpServer.closeIdleConnections?.();
|
|
397
|
+
httpServer.closeAllConnections?.();
|
|
398
|
+
for (const socket of httpConnections) {
|
|
399
|
+
socket.destroy();
|
|
400
|
+
}
|
|
385
401
|
}
|
|
386
402
|
|
|
387
403
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
package/src/terminal-manager.mjs
CHANGED
|
@@ -9,13 +9,12 @@ import * as persistence from './persistence.mjs';
|
|
|
9
9
|
import { config } from './config.mjs';
|
|
10
10
|
|
|
11
11
|
function resolveShell() {
|
|
12
|
-
if (process.platform === 'win32') {
|
|
13
|
-
return process.env.COMSPEC || 'cmd.exe';
|
|
14
|
-
}
|
|
15
|
-
// config.shell has already handled process.env.SHELL and TABMINAL_SHELL via config.mjs
|
|
16
12
|
if (config.shell) {
|
|
17
13
|
return config.shell;
|
|
18
14
|
}
|
|
15
|
+
if (process.platform === 'win32') {
|
|
16
|
+
return process.env.COMSPEC || 'cmd.exe';
|
|
17
|
+
}
|
|
19
18
|
// Try to use Homebrew installed bash if available (newer version)
|
|
20
19
|
if (fs.existsSync('/opt/homebrew/bin/bash')) {
|
|
21
20
|
return '/opt/homebrew/bin/bash';
|
|
@@ -38,6 +37,25 @@ const initialRows = Number.parseInt(
|
|
|
38
37
|
10
|
|
39
38
|
) || 30;
|
|
40
39
|
|
|
40
|
+
function buildBashBootstrap({
|
|
41
|
+
env,
|
|
42
|
+
shell,
|
|
43
|
+
shellToolsPath,
|
|
44
|
+
sessionId
|
|
45
|
+
}) {
|
|
46
|
+
const hookPath = path.join(shellToolsPath, 'tabminal-hooks.bash');
|
|
47
|
+
const rcfilePath = path.join(shellToolsPath, 'tabminal-bashrc');
|
|
48
|
+
|
|
49
|
+
env.TABMINAL_SESSION_ID = sessionId;
|
|
50
|
+
env.TABMINAL_SHELL_TOOLS_PATH = shellToolsPath;
|
|
51
|
+
env.TABMINAL_HOOKS_PATH = hookPath;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
shell,
|
|
55
|
+
args: ['--rcfile', rcfilePath, '-i']
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
export class TerminalManager {
|
|
42
60
|
constructor() {
|
|
43
61
|
this.sessions = new Map();
|
|
@@ -60,57 +78,32 @@ export class TerminalManager {
|
|
|
60
78
|
|
|
61
79
|
// Inject shell tools
|
|
62
80
|
const shellToolsPath = path.join(process.cwd(), 'shell');
|
|
63
|
-
|
|
81
|
+
const pathDelimiter = path.delimiter;
|
|
82
|
+
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'PATH';
|
|
83
|
+
const existingPath = env[pathKey];
|
|
84
|
+
env[pathKey] = existingPath
|
|
85
|
+
? `${shellToolsPath}${pathDelimiter}${existingPath}`
|
|
86
|
+
: shellToolsPath;
|
|
64
87
|
|
|
88
|
+
let spawnShell = shell;
|
|
65
89
|
let args = [];
|
|
66
|
-
let initFilePath = null;
|
|
67
90
|
let initDirPath = null;
|
|
68
91
|
|
|
69
92
|
try {
|
|
70
93
|
const shellName = path.basename(shell);
|
|
71
94
|
if (shellName === 'bash') {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if [[ "$BASH_COMMAND" == *"_tabminal_"* || "$BASH_COMMAND" == "$PROMPT_COMMAND" ]]; then
|
|
81
|
-
return
|
|
82
|
-
fi
|
|
83
|
-
_tabminal_last_command="$BASH_COMMAND"
|
|
84
|
-
}
|
|
85
|
-
trap '_tabminal_bash_preexec' DEBUG
|
|
86
|
-
|
|
87
|
-
_tabminal_bash_postexec() {
|
|
88
|
-
local EC="$?"
|
|
89
|
-
if [[ -n "$_tabminal_last_command" ]]; then
|
|
90
|
-
local CMD=$(echo -n "$_tabminal_last_command" | base64 | tr -d '\\n')
|
|
91
|
-
printf "\\x1b]1337;ExitCode=%s;CommandB64=%s\\x07" "$EC" "$CMD"
|
|
92
|
-
_tabminal_last_command="" # Reset after use
|
|
93
|
-
fi
|
|
94
|
-
}
|
|
95
|
-
_tabminal_apply_prompt_marker() {
|
|
96
|
-
local marker=$'\\[\\e]1337;TabminalPrompt\\a\\]'
|
|
97
|
-
if [[ "$PS1" != *"TabminalPrompt"* ]]; then
|
|
98
|
-
PS1="$PS1$marker"
|
|
99
|
-
fi
|
|
100
|
-
}
|
|
101
|
-
if [[ -n "$PROMPT_COMMAND" ]]; then
|
|
102
|
-
printf -v PROMPT_COMMAND "_tabminal_bash_postexec; %s; _tabminal_apply_prompt_marker" "$PROMPT_COMMAND"
|
|
103
|
-
else
|
|
104
|
-
PROMPT_COMMAND="_tabminal_bash_postexec; _tabminal_apply_prompt_marker"
|
|
105
|
-
fi
|
|
106
|
-
export PROMPT_COMMAND
|
|
107
|
-
`;
|
|
108
|
-
fs.writeFileSync(initFilePath, bashScript);
|
|
109
|
-
args = ['--rcfile', initFilePath, '-i'];
|
|
95
|
+
const bootstrap = buildBashBootstrap({
|
|
96
|
+
env,
|
|
97
|
+
shell,
|
|
98
|
+
shellToolsPath,
|
|
99
|
+
sessionId: id
|
|
100
|
+
});
|
|
101
|
+
spawnShell = bootstrap.shell;
|
|
102
|
+
args = bootstrap.args;
|
|
110
103
|
} else if (shellName === 'zsh') {
|
|
111
104
|
initDirPath = path.join(os.tmpdir(), `tabminal-zsh-${id}`);
|
|
112
105
|
fs.mkdirSync(initDirPath, { recursive: true });
|
|
113
|
-
initFilePath = path.join(initDirPath, '.zshrc');
|
|
106
|
+
const initFilePath = path.join(initDirPath, '.zshrc');
|
|
114
107
|
|
|
115
108
|
const zshScript = `
|
|
116
109
|
unset ZDOTDIR
|
|
@@ -151,17 +144,21 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
151
144
|
|
|
152
145
|
let ptyProcess;
|
|
153
146
|
try {
|
|
154
|
-
|
|
147
|
+
const ptyOptions = {
|
|
155
148
|
name: 'xterm-256color',
|
|
156
149
|
cols: cols,
|
|
157
150
|
rows: rows,
|
|
158
151
|
cwd: initialCwd,
|
|
159
|
-
env: env
|
|
160
|
-
|
|
161
|
-
|
|
152
|
+
env: env
|
|
153
|
+
};
|
|
154
|
+
if (process.platform !== 'win32') {
|
|
155
|
+
ptyOptions.encoding = 'utf8';
|
|
156
|
+
}
|
|
157
|
+
ptyProcess = pty.spawn(spawnShell, args, ptyOptions);
|
|
162
158
|
} catch (err) {
|
|
163
159
|
const spawnInfo = {
|
|
164
|
-
shell,
|
|
160
|
+
shell: spawnShell,
|
|
161
|
+
requestedShell: shell,
|
|
165
162
|
args,
|
|
166
163
|
cwd: initialCwd,
|
|
167
164
|
cols,
|
|
@@ -207,7 +204,6 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
207
204
|
this.removeSession(id);
|
|
208
205
|
// Cleanup temp files
|
|
209
206
|
try {
|
|
210
|
-
if (initFilePath && fs.existsSync(initFilePath)) fs.unlinkSync(initFilePath);
|
|
211
207
|
if (initDirPath && fs.existsSync(initDirPath)) fs.rmSync(initDirPath, { recursive: true, force: true });
|
|
212
208
|
} catch { /* ignore cleanup errors */ }
|
|
213
209
|
});
|
|
@@ -295,7 +291,11 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
295
291
|
this.disposing = true;
|
|
296
292
|
for (const session of this.sessions.values()) {
|
|
297
293
|
try {
|
|
298
|
-
|
|
294
|
+
if (process.platform === 'win32') {
|
|
295
|
+
session.pty.kill();
|
|
296
|
+
} else {
|
|
297
|
+
session.pty.kill('SIGHUP');
|
|
298
|
+
}
|
|
299
299
|
} catch {
|
|
300
300
|
// ignore
|
|
301
301
|
}
|
package/src/terminal-session.mjs
CHANGED
|
@@ -20,11 +20,31 @@ const TITLE_POLL_INTERVAL_MS = 3000;
|
|
|
20
20
|
|
|
21
21
|
const IGNORED_COMMANDS = [
|
|
22
22
|
'export PROMPT_COMMAND',
|
|
23
|
-
'__bash_prompt'
|
|
23
|
+
'__bash_prompt',
|
|
24
|
+
'TABMINAL_SHELL_READY=1'
|
|
24
25
|
];
|
|
25
26
|
|
|
26
27
|
const PROMPT_PREFIX = "You are now operating as an AI terminal assistant. Your name is `Tabminal`. You will assist users in resolving terminal or coding issues and answering other inquiries. When troubleshooting terminal errors, you will be provided with the execution history to understand the context. However, please focus primarily on the most recent runtime errors and the user's latest questions. Keep your answers concise and accurate. Resolve the issue clearly and provide the reasoning while avoiding lengthy elaborations. Most user terminal variable keys are normal under typical circumstances and do not need to be treated as security risks.\n\n";
|
|
27
28
|
|
|
29
|
+
function splitTrailingPartialSequence(chunk) {
|
|
30
|
+
if (!chunk) {
|
|
31
|
+
return { complete: '', partial: '' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const oscStart = chunk.lastIndexOf('\u001b]1337;');
|
|
35
|
+
if (oscStart >= 0) {
|
|
36
|
+
const oscTail = chunk.slice(oscStart);
|
|
37
|
+
if (!oscTail.includes('\u0007')) {
|
|
38
|
+
return {
|
|
39
|
+
complete: chunk.slice(0, oscStart),
|
|
40
|
+
partial: oscTail
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { complete: chunk, partial: '' };
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
export class TerminalSession {
|
|
29
49
|
constructor(pty, options = {}) {
|
|
30
50
|
this.pty = pty;
|
|
@@ -55,6 +75,7 @@ export class TerminalSession {
|
|
|
55
75
|
this.captureStartedAt = null;
|
|
56
76
|
this.lastExecution = null;
|
|
57
77
|
this.skipNextShellLog = false;
|
|
78
|
+
this.partialSequenceBuffer = '';
|
|
58
79
|
|
|
59
80
|
this.ansiParser = new AnsiParser({
|
|
60
81
|
inst_o: (s) => {
|
|
@@ -83,6 +104,13 @@ export class TerminalSession {
|
|
|
83
104
|
if (this.suppressPtyOutput) return;
|
|
84
105
|
|
|
85
106
|
if (typeof chunk !== 'string') chunk = chunk.toString('utf8');
|
|
107
|
+
if (this.partialSequenceBuffer) {
|
|
108
|
+
chunk = this.partialSequenceBuffer + chunk;
|
|
109
|
+
this.partialSequenceBuffer = '';
|
|
110
|
+
}
|
|
111
|
+
const split = splitTrailingPartialSequence(chunk);
|
|
112
|
+
chunk = split.complete;
|
|
113
|
+
this.partialSequenceBuffer = split.partial;
|
|
86
114
|
|
|
87
115
|
if (this.manager) {
|
|
88
116
|
this.manager.appendLog(this.id, chunk);
|