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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tabminal",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
4
4
  "description": "A modern, persistent web terminal with multi-tab support and real-time system monitoring.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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'));
@@ -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
- env.PATH = `${shellToolsPath}:${env.PATH}`;
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
- initFilePath = path.join(os.tmpdir(), `tabminal-init-${id}.bashrc`);
73
- const bashScript = `
74
- export PATH="${shellToolsPath}:$PATH"
75
- [ -f ~/.bashrc ] && source ~/.bashrc
76
- export PATH="${shellToolsPath}:$PATH"
77
-
78
- _tabminal_bash_preexec() {
79
- # Prevent capturing any of our own internal or setup commands.
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
- ptyProcess = pty.spawn(shell, args, {
147
+ const ptyOptions = {
155
148
  name: 'xterm-256color',
156
149
  cols: cols,
157
150
  rows: rows,
158
151
  cwd: initialCwd,
159
- env: env,
160
- encoding: 'utf8'
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
- session.pty.kill('SIGHUP');
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
  }
@@ -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);