x-shell.js 1.0.0 → 1.1.0
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/CHANGELOG.md +45 -0
- package/README.md +292 -2
- package/dist/client/browser-bundle.js +197 -3
- package/dist/client/browser-bundle.js.map +2 -2
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/terminal-client.d.ts +59 -3
- package/dist/client/terminal-client.d.ts.map +1 -1
- package/dist/client/terminal-client.js +194 -4
- package/dist/client/terminal-client.js.map +1 -1
- package/dist/server/circular-buffer.d.ts +55 -0
- package/dist/server/circular-buffer.d.ts.map +1 -0
- package/dist/server/circular-buffer.js +91 -0
- package/dist/server/circular-buffer.js.map +1 -0
- package/dist/server/index.d.ts +4 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/session-manager.d.ts +195 -0
- package/dist/server/session-manager.d.ts.map +1 -0
- package/dist/server/session-manager.js +448 -0
- package/dist/server/session-manager.js.map +1 -0
- package/dist/server/terminal-server.d.ts +54 -6
- package/dist/server/terminal-server.d.ts.map +1 -1
- package/dist/server/terminal-server.js +313 -80
- package/dist/server/terminal-server.js.map +1 -1
- package/dist/shared/types.d.ts +122 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/ui/browser-bundle.js +745 -47
- package/dist/ui/browser-bundle.js.map +3 -3
- package/dist/ui/x-shell-terminal.d.ts +99 -1
- package/dist/ui/x-shell-terminal.d.ts.map +1 -1
- package/dist/ui/x-shell-terminal.js +604 -48
- package/dist/ui/x-shell-terminal.js.map +1 -1
- package/package.json +5 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.1.0] - 2025-01-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Tabbed Terminals**: New `show-tabs` attribute enables multiple terminal tabs in a single component
|
|
13
|
+
- Each tab has independent WebSocket connection and terminal session
|
|
14
|
+
- Tab bar with status indicators and add/close buttons
|
|
15
|
+
- Dynamic labels showing shell or container name
|
|
16
|
+
- `createTab()`, `switchTab()`, `closeTab()` methods
|
|
17
|
+
- **Join Existing Session**: Connection panel now shows "Join Existing Session" mode when sessions are available
|
|
18
|
+
- **Prompt Refresh on Join**: Joining a session now triggers a fresh prompt display
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Fixed duplicate output when multiple tabs join the same session
|
|
23
|
+
- Fixed session list not updating after spawning a session
|
|
24
|
+
- Each client's data handler now correctly writes to its own tab's terminal
|
|
25
|
+
|
|
26
|
+
## [1.0.0] - 2025-01-08
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Initial release
|
|
31
|
+
- WebSocket-based terminal server with node-pty
|
|
32
|
+
- Lightweight client library with auto-reconnection
|
|
33
|
+
- `<x-shell-terminal>` Lit web component with xterm.js
|
|
34
|
+
- Docker exec support for connecting to containers
|
|
35
|
+
- Docker attach mode for connecting to container's main process (PID 1)
|
|
36
|
+
- Session multiplexing - multiple clients sharing the same terminal
|
|
37
|
+
- Session persistence with configurable orphan timeout
|
|
38
|
+
- History replay for clients joining existing sessions
|
|
39
|
+
- Built-in connection panel with container/shell selector
|
|
40
|
+
- Settings dropdown (theme, font size)
|
|
41
|
+
- Status bar with connection info
|
|
42
|
+
- Dark/light/auto theme support
|
|
43
|
+
- Security features: shell, path, and container allowlists
|
|
44
|
+
- Python client bindings (`bindings/python/`)
|
|
45
|
+
- Example projects for Docker containers and multiplexing
|
package/README.md
CHANGED
|
@@ -9,7 +9,12 @@ A plug-and-play terminal solution for web applications. Includes a server compon
|
|
|
9
9
|
- **Server**: WebSocket server with node-pty for real shell sessions
|
|
10
10
|
- **Client**: Lightweight WebSocket client with auto-reconnection
|
|
11
11
|
- **UI**: `<x-shell-terminal>` Lit web component with xterm.js
|
|
12
|
-
- **
|
|
12
|
+
- **Tabbed Terminals**: Multiple terminal tabs in a single component
|
|
13
|
+
- **Docker Exec**: Connect to Docker containers via `docker exec`
|
|
14
|
+
- **Docker Attach**: Connect to a container's main process (PID 1)
|
|
15
|
+
- **Session Multiplexing**: Multiple clients can share the same terminal session
|
|
16
|
+
- **Session Persistence**: Sessions survive client disconnects with configurable timeout
|
|
17
|
+
- **History Replay**: New clients receive recent terminal output when joining
|
|
13
18
|
- **Themes**: Built-in dark/light/auto theme support
|
|
14
19
|
- **Security**: Configurable shell, path, and container allowlists
|
|
15
20
|
- **Framework Agnostic**: Works with React, Vue, Angular, Svelte, or vanilla JS
|
|
@@ -58,7 +63,7 @@ The UI component can be loaded directly from a CDN - no build step required:
|
|
|
58
63
|
<script type="module" src="https://cdn.jsdelivr.net/npm/x-shell.js/dist/ui/browser-bundle.js"></script>
|
|
59
64
|
|
|
60
65
|
<!-- Pin to a specific version -->
|
|
61
|
-
<script type="module" src="https://unpkg.com/x-shell.js@1.0.0
|
|
66
|
+
<script type="module" src="https://unpkg.com/x-shell.js@1.0.0/dist/ui/browser-bundle.js"></script>
|
|
62
67
|
```
|
|
63
68
|
|
|
64
69
|
The bundle includes the `<x-shell-terminal>` web component with xterm.js built-in.
|
|
@@ -169,6 +174,13 @@ const server = new TerminalServer({
|
|
|
169
174
|
|
|
170
175
|
// Enable verbose logging
|
|
171
176
|
verbose: false,
|
|
177
|
+
|
|
178
|
+
// Session multiplexing options
|
|
179
|
+
maxClientsPerSession: 10, // Max clients per session (default: 10)
|
|
180
|
+
orphanTimeout: 60000, // Ms before orphaned sessions close (default: 60000)
|
|
181
|
+
historySize: 50000, // History buffer size in chars (default: 50000)
|
|
182
|
+
historyEnabled: true, // Enable history replay (default: true)
|
|
183
|
+
maxSessionsTotal: 100, // Max concurrent sessions (default: 100)
|
|
172
184
|
});
|
|
173
185
|
|
|
174
186
|
// Attach to HTTP server
|
|
@@ -235,6 +247,20 @@ client.isConnected(); // boolean
|
|
|
235
247
|
client.hasActiveSession(); // boolean
|
|
236
248
|
client.getSessionId(); // string | null
|
|
237
249
|
client.getSessionInfo(); // SessionInfo | null
|
|
250
|
+
|
|
251
|
+
// Session multiplexing
|
|
252
|
+
const sessions = await client.listSessions(); // List all sessions
|
|
253
|
+
const session = await client.join({ // Join existing session
|
|
254
|
+
sessionId: 'term-123...',
|
|
255
|
+
requestHistory: true,
|
|
256
|
+
historyLimit: 50000,
|
|
257
|
+
});
|
|
258
|
+
client.leave(); // Leave without killing
|
|
259
|
+
|
|
260
|
+
// Multiplexing event handlers
|
|
261
|
+
client.onClientJoined((sessionId, count) => console.log(`${count} clients`));
|
|
262
|
+
client.onClientLeft((sessionId, count) => console.log(`${count} clients`));
|
|
263
|
+
client.onSessionClosed((sessionId, reason) => console.log(reason));
|
|
238
264
|
```
|
|
239
265
|
|
|
240
266
|
### UI Component
|
|
@@ -278,6 +304,7 @@ client.getSessionInfo(); // SessionInfo | null
|
|
|
278
304
|
| `show-connection-panel` | boolean | `false` | Show connection panel with container/shell selector |
|
|
279
305
|
| `show-settings` | boolean | `false` | Show settings dropdown (theme, font size) |
|
|
280
306
|
| `show-status-bar` | boolean | `false` | Show status bar with connection info and errors |
|
|
307
|
+
| `show-tabs` | boolean | `false` | Enable tabbed terminal interface |
|
|
281
308
|
|
|
282
309
|
**Methods:**
|
|
283
310
|
|
|
@@ -292,6 +319,11 @@ terminal.clear(); // Clear display
|
|
|
292
319
|
terminal.write('text'); // Write to display
|
|
293
320
|
terminal.writeln('line'); // Write line to display
|
|
294
321
|
terminal.focus(); // Focus terminal
|
|
322
|
+
|
|
323
|
+
// Tab methods (when show-tabs is enabled)
|
|
324
|
+
terminal.createTab('label'); // Create new tab
|
|
325
|
+
terminal.switchTab('tab-id'); // Switch to tab
|
|
326
|
+
terminal.closeTab('tab-id'); // Close tab
|
|
295
327
|
```
|
|
296
328
|
|
|
297
329
|
**Events:**
|
|
@@ -328,6 +360,68 @@ The connection panel automatically queries the server for:
|
|
|
328
360
|
></x-shell-terminal>
|
|
329
361
|
```
|
|
330
362
|
|
|
363
|
+
## Tabbed Terminals
|
|
364
|
+
|
|
365
|
+
Enable multiple terminal tabs within a single component using the `show-tabs` attribute:
|
|
366
|
+
|
|
367
|
+
```html
|
|
368
|
+
<x-shell-terminal
|
|
369
|
+
url="ws://localhost:3000/terminal"
|
|
370
|
+
show-tabs
|
|
371
|
+
show-connection-panel
|
|
372
|
+
show-settings
|
|
373
|
+
show-status-bar
|
|
374
|
+
></x-shell-terminal>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Features
|
|
378
|
+
|
|
379
|
+
- **Independent Sessions**: Each tab has its own WebSocket connection and terminal session
|
|
380
|
+
- **Tab Bar**: Shows all open tabs with status indicators
|
|
381
|
+
- **Dynamic Labels**: Tabs automatically update their label to show the shell or container name
|
|
382
|
+
- **Session Joining**: Create a tab and join an existing session from another tab
|
|
383
|
+
- **Easy Management**: Click "+" to add tabs, "×" to close, click tab to switch
|
|
384
|
+
|
|
385
|
+
### Tab API
|
|
386
|
+
|
|
387
|
+
```javascript
|
|
388
|
+
const terminal = document.querySelector('x-shell-terminal');
|
|
389
|
+
|
|
390
|
+
// Create a new tab
|
|
391
|
+
const tab = terminal.createTab('My Terminal');
|
|
392
|
+
// Returns: { id: 'tab-1', label: 'My Terminal', ... }
|
|
393
|
+
|
|
394
|
+
// Switch to a specific tab
|
|
395
|
+
terminal.switchTab('tab-1');
|
|
396
|
+
|
|
397
|
+
// Close a tab (resources are cleaned up automatically)
|
|
398
|
+
terminal.closeTab('tab-1');
|
|
399
|
+
|
|
400
|
+
// Access tab state
|
|
401
|
+
// Each tab maintains its own: client, terminal, sessionInfo, etc.
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Use Cases
|
|
405
|
+
|
|
406
|
+
**1. Multi-Environment Development**
|
|
407
|
+
```html
|
|
408
|
+
<!-- Open tabs for different containers -->
|
|
409
|
+
<x-shell-terminal show-tabs show-connection-panel></x-shell-terminal>
|
|
410
|
+
```
|
|
411
|
+
- Tab 1: Local shell for git operations
|
|
412
|
+
- Tab 2: Docker container for backend
|
|
413
|
+
- Tab 3: Docker container for frontend
|
|
414
|
+
|
|
415
|
+
**2. Session Sharing**
|
|
416
|
+
- Create a session in Tab 1
|
|
417
|
+
- Create Tab 2, select "Join Existing Session"
|
|
418
|
+
- Both tabs now mirror the same terminal
|
|
419
|
+
|
|
420
|
+
**3. Monitoring Multiple Processes**
|
|
421
|
+
- Open multiple tabs
|
|
422
|
+
- Each tab connects to a different running session
|
|
423
|
+
- Monitor all processes from a single interface
|
|
424
|
+
|
|
331
425
|
## Theming
|
|
332
426
|
|
|
333
427
|
The component uses CSS custom properties for theming:
|
|
@@ -436,6 +530,178 @@ const server = new TerminalServer({
|
|
|
436
530
|
});
|
|
437
531
|
```
|
|
438
532
|
|
|
533
|
+
### Docker Attach Mode
|
|
534
|
+
|
|
535
|
+
Docker attach connects to a container's main process (PID 1) instead of spawning a new shell. This is useful for:
|
|
536
|
+
- Interacting with interactive containers started with `docker run -it`
|
|
537
|
+
- Debugging container startup issues
|
|
538
|
+
- Sharing a session with `docker attach` from another terminal
|
|
539
|
+
|
|
540
|
+
```javascript
|
|
541
|
+
// Client: Attach to container's main process
|
|
542
|
+
await client.spawn({
|
|
543
|
+
container: 'my-container',
|
|
544
|
+
attachMode: true, // Use docker attach instead of docker exec
|
|
545
|
+
});
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Important:** Docker attach connects to whatever is running as PID 1. If the container was started with a non-interactive command (like a web server), attach may not provide useful interaction.
|
|
549
|
+
|
|
550
|
+
```bash
|
|
551
|
+
# Start an interactive container first
|
|
552
|
+
docker run -it --name demo alpine sh
|
|
553
|
+
|
|
554
|
+
# Then attach via x-shell
|
|
555
|
+
# Both terminals now share the same shell session
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
## Session Multiplexing
|
|
559
|
+
|
|
560
|
+
Session multiplexing allows multiple clients to connect to the same terminal session. This enables:
|
|
561
|
+
- **Collaboration**: Multiple users can share a terminal
|
|
562
|
+
- **Session Persistence**: Sessions survive client disconnects
|
|
563
|
+
- **History Replay**: New clients receive recent output when joining
|
|
564
|
+
- **Monitoring**: Watch others' terminal sessions in real-time
|
|
565
|
+
|
|
566
|
+
### How It Works
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
┌──────────┐ ┌─────────────────────────────────────────┐ ┌──────────┐
|
|
570
|
+
│ Client A │◄────┤ SessionManager ├────►│ PTY │
|
|
571
|
+
└──────────┘ │ ┌─────────────────────────────────────┐ │ │ Process │
|
|
572
|
+
│ │ SharedSession │ │ └──────────┘
|
|
573
|
+
┌──────────┐ │ │ - clients: [A, B, C] │ │
|
|
574
|
+
│ Client B │◄────┼──┤ - historyBuffer (50KB) │ │
|
|
575
|
+
└──────────┘ │ │ - orphanedAt: null │ │
|
|
576
|
+
│ └─────────────────────────────────────┘ │
|
|
577
|
+
┌──────────┐ │ │
|
|
578
|
+
│ Client C │◄────┼─────────────────────────────────────────┘
|
|
579
|
+
└──────────┘ (broadcast output)
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Server Configuration
|
|
583
|
+
|
|
584
|
+
```javascript
|
|
585
|
+
const server = new TerminalServer({
|
|
586
|
+
// Maximum clients that can join a single session
|
|
587
|
+
maxClientsPerSession: 10,
|
|
588
|
+
|
|
589
|
+
// Time before orphaned session is killed (ms)
|
|
590
|
+
// Set to 0 to kill immediately on last client disconnect
|
|
591
|
+
orphanTimeout: 60000,
|
|
592
|
+
|
|
593
|
+
// History buffer size for replay on join
|
|
594
|
+
historySize: 50000,
|
|
595
|
+
|
|
596
|
+
// Enable/disable history feature
|
|
597
|
+
historyEnabled: true,
|
|
598
|
+
|
|
599
|
+
// Maximum total sessions across all clients
|
|
600
|
+
maxSessionsTotal: 100,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Get session statistics
|
|
604
|
+
const stats = server.getStats();
|
|
605
|
+
// { sessionCount: 5, clientCount: 12, orphanedCount: 1 }
|
|
606
|
+
|
|
607
|
+
// List all sessions with multiplexing info
|
|
608
|
+
const sessions = server.getSharedSessions();
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Client API
|
|
612
|
+
|
|
613
|
+
```javascript
|
|
614
|
+
// List available sessions
|
|
615
|
+
const sessions = await client.listSessions();
|
|
616
|
+
// Filter by type or container
|
|
617
|
+
const dockerSessions = await client.listSessions({ type: 'docker-exec' });
|
|
618
|
+
|
|
619
|
+
// Create a shareable session
|
|
620
|
+
await client.spawn({
|
|
621
|
+
shell: '/bin/bash',
|
|
622
|
+
label: 'dev-session', // Optional label for identification
|
|
623
|
+
allowJoin: true, // Allow others to join (default: true)
|
|
624
|
+
enableHistory: true, // Enable history buffer (default: true)
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Join an existing session
|
|
628
|
+
const session = await client.join({
|
|
629
|
+
sessionId: 'term-abc123...',
|
|
630
|
+
requestHistory: true, // Request output history
|
|
631
|
+
historyLimit: 50000, // Max history chars to receive
|
|
632
|
+
});
|
|
633
|
+
// session.history contains recent output
|
|
634
|
+
|
|
635
|
+
// Leave session without killing it
|
|
636
|
+
client.leave();
|
|
637
|
+
// Session survives if other clients connected
|
|
638
|
+
// Or waits orphanTimeout before closing
|
|
639
|
+
|
|
640
|
+
// Kill session (only owner can do this)
|
|
641
|
+
client.kill();
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Spawn Options for Multiplexing
|
|
645
|
+
|
|
646
|
+
| Option | Type | Default | Description |
|
|
647
|
+
|--------|------|---------|-------------|
|
|
648
|
+
| `label` | string | - | Session label for identification |
|
|
649
|
+
| `allowJoin` | boolean | `true` | Allow other clients to join |
|
|
650
|
+
| `enableHistory` | boolean | `true` | Enable history buffer |
|
|
651
|
+
| `attachMode` | boolean | `false` | Use docker attach instead of exec |
|
|
652
|
+
|
|
653
|
+
### Event Handlers
|
|
654
|
+
|
|
655
|
+
```javascript
|
|
656
|
+
// Called when another client joins your session
|
|
657
|
+
client.onClientJoined((sessionId, clientCount) => {
|
|
658
|
+
console.log(`Client joined, ${clientCount} total`);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// Called when another client leaves
|
|
662
|
+
client.onClientLeft((sessionId, clientCount) => {
|
|
663
|
+
console.log(`Client left, ${clientCount} remaining`);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Called when session is closed
|
|
667
|
+
client.onSessionClosed((sessionId, reason) => {
|
|
668
|
+
// reason: 'orphan_timeout' | 'owner_closed' | 'process_exit' | 'error'
|
|
669
|
+
console.log(`Session closed: ${reason}`);
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### Use Cases
|
|
674
|
+
|
|
675
|
+
**1. Pair Programming**
|
|
676
|
+
```javascript
|
|
677
|
+
// Developer A creates session
|
|
678
|
+
await client.spawn({ label: 'pair-session' });
|
|
679
|
+
// Share session ID with Developer B
|
|
680
|
+
// Developer B joins with history
|
|
681
|
+
await client.join({ sessionId, requestHistory: true });
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
**2. Session Persistence**
|
|
685
|
+
```javascript
|
|
686
|
+
// Start long-running task
|
|
687
|
+
await client.spawn({ shell: '/bin/bash' });
|
|
688
|
+
client.write('npm run build\n');
|
|
689
|
+
client.disconnect(); // Session survives!
|
|
690
|
+
|
|
691
|
+
// Later, reconnect
|
|
692
|
+
await client.connect();
|
|
693
|
+
const sessions = await client.listSessions();
|
|
694
|
+
await client.join({ sessionId: sessions[0].sessionId, requestHistory: true });
|
|
695
|
+
// See build output that happened while disconnected
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**3. Monitoring**
|
|
699
|
+
```javascript
|
|
700
|
+
// Admin joins session in read-only mode
|
|
701
|
+
await client.join({ sessionId, requestHistory: true });
|
|
702
|
+
// Watch activity without interfering
|
|
703
|
+
```
|
|
704
|
+
|
|
439
705
|
## Security
|
|
440
706
|
|
|
441
707
|
**Always configure security for production:**
|
|
@@ -461,6 +727,30 @@ const server = new TerminalServer({
|
|
|
461
727
|
See the [examples](./examples) directory for complete working examples:
|
|
462
728
|
|
|
463
729
|
- [**docker-container**](./examples/docker-container) - Connect to Docker containers from the browser
|
|
730
|
+
- [**multiplexing**](./examples/multiplexing) - Session multiplexing with multiple clients sharing terminals
|
|
731
|
+
|
|
732
|
+
### Running Locally (Development)
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
# Clone the repository
|
|
736
|
+
git clone https://github.com/lsadehaan/x-shell.git
|
|
737
|
+
cd x-shell
|
|
738
|
+
|
|
739
|
+
# Install dependencies (including node-pty)
|
|
740
|
+
npm install
|
|
741
|
+
npm install node-pty --save-dev --legacy-peer-deps
|
|
742
|
+
|
|
743
|
+
# Build the project
|
|
744
|
+
npm run build
|
|
745
|
+
|
|
746
|
+
# Start a test container (optional, for Docker exec testing)
|
|
747
|
+
docker run -d --name test-container alpine sleep infinity
|
|
748
|
+
|
|
749
|
+
# Run the example server
|
|
750
|
+
node examples/docker-container/server.js
|
|
751
|
+
|
|
752
|
+
# Open http://localhost:3000 in your browser
|
|
753
|
+
```
|
|
464
754
|
|
|
465
755
|
### Quick Start with Docker Compose
|
|
466
756
|
|
|
@@ -17,9 +17,19 @@ var TerminalClient = class {
|
|
|
17
17
|
this.spawnedHandlers = [];
|
|
18
18
|
this.serverInfoHandlers = [];
|
|
19
19
|
this.containerListHandlers = [];
|
|
20
|
-
//
|
|
20
|
+
// Session multiplexing handlers
|
|
21
|
+
this.sessionListHandlers = [];
|
|
22
|
+
this.joinedHandlers = [];
|
|
23
|
+
this.leftHandlers = [];
|
|
24
|
+
this.clientJoinedHandlers = [];
|
|
25
|
+
this.clientLeftHandlers = [];
|
|
26
|
+
this.sessionClosedHandlers = [];
|
|
27
|
+
// Promise resolvers for spawn/join
|
|
21
28
|
this.spawnResolve = null;
|
|
22
29
|
this.spawnReject = null;
|
|
30
|
+
this.joinResolve = null;
|
|
31
|
+
this.joinReject = null;
|
|
32
|
+
this.listSessionsResolve = null;
|
|
23
33
|
this.config = {
|
|
24
34
|
url: config.url,
|
|
25
35
|
reconnect: config.reconnect ?? true,
|
|
@@ -153,6 +163,11 @@ var TerminalClient = class {
|
|
|
153
163
|
this.spawnResolve = null;
|
|
154
164
|
this.spawnReject = null;
|
|
155
165
|
}
|
|
166
|
+
if (this.joinReject) {
|
|
167
|
+
this.joinReject(error);
|
|
168
|
+
this.joinResolve = null;
|
|
169
|
+
this.joinReject = null;
|
|
170
|
+
}
|
|
156
171
|
break;
|
|
157
172
|
case "serverInfo":
|
|
158
173
|
this.serverInfo = message.info;
|
|
@@ -161,6 +176,64 @@ var TerminalClient = class {
|
|
|
161
176
|
case "containerList":
|
|
162
177
|
this.containerListHandlers.forEach((handler) => handler(message.containers));
|
|
163
178
|
break;
|
|
179
|
+
case "sessionList":
|
|
180
|
+
this.sessionListHandlers.forEach(
|
|
181
|
+
(handler) => handler(message.sessions)
|
|
182
|
+
);
|
|
183
|
+
if (this.listSessionsResolve) {
|
|
184
|
+
this.listSessionsResolve(message.sessions);
|
|
185
|
+
this.listSessionsResolve = null;
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
case "joined":
|
|
189
|
+
const joinedSession = message.session;
|
|
190
|
+
const history = message.history;
|
|
191
|
+
this.sessionId = message.sessionId;
|
|
192
|
+
this.sessionInfo = {
|
|
193
|
+
sessionId: joinedSession.sessionId,
|
|
194
|
+
shell: joinedSession.shell,
|
|
195
|
+
cwd: joinedSession.cwd,
|
|
196
|
+
cols: joinedSession.cols,
|
|
197
|
+
rows: joinedSession.rows,
|
|
198
|
+
createdAt: joinedSession.createdAt,
|
|
199
|
+
container: joinedSession.container
|
|
200
|
+
};
|
|
201
|
+
this.joinedHandlers.forEach((handler) => handler(joinedSession, history));
|
|
202
|
+
if (this.joinResolve) {
|
|
203
|
+
this.joinResolve(joinedSession);
|
|
204
|
+
this.joinResolve = null;
|
|
205
|
+
this.joinReject = null;
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
case "left":
|
|
209
|
+
const leftSessionId = message.sessionId;
|
|
210
|
+
if (this.sessionId === leftSessionId) {
|
|
211
|
+
this.sessionId = null;
|
|
212
|
+
this.sessionInfo = null;
|
|
213
|
+
}
|
|
214
|
+
this.leftHandlers.forEach((handler) => handler(leftSessionId));
|
|
215
|
+
break;
|
|
216
|
+
case "clientJoined":
|
|
217
|
+
this.clientJoinedHandlers.forEach(
|
|
218
|
+
(handler) => handler(message.sessionId, message.clientCount)
|
|
219
|
+
);
|
|
220
|
+
break;
|
|
221
|
+
case "clientLeft":
|
|
222
|
+
this.clientLeftHandlers.forEach(
|
|
223
|
+
(handler) => handler(message.sessionId, message.clientCount)
|
|
224
|
+
);
|
|
225
|
+
break;
|
|
226
|
+
case "sessionClosed":
|
|
227
|
+
const closedSessionId = message.sessionId;
|
|
228
|
+
const reason = message.reason;
|
|
229
|
+
if (this.sessionId === closedSessionId) {
|
|
230
|
+
this.sessionId = null;
|
|
231
|
+
this.sessionInfo = null;
|
|
232
|
+
}
|
|
233
|
+
this.sessionClosedHandlers.forEach(
|
|
234
|
+
(handler) => handler(closedSessionId, reason)
|
|
235
|
+
);
|
|
236
|
+
break;
|
|
164
237
|
}
|
|
165
238
|
}
|
|
166
239
|
/**
|
|
@@ -173,7 +246,7 @@ var TerminalClient = class {
|
|
|
173
246
|
return;
|
|
174
247
|
}
|
|
175
248
|
if (this.sessionId) {
|
|
176
|
-
reject(new Error("Session already
|
|
249
|
+
reject(new Error("Session already active. Call kill() or leave() first."));
|
|
177
250
|
return;
|
|
178
251
|
}
|
|
179
252
|
this.spawnResolve = resolve;
|
|
@@ -228,7 +301,7 @@ var TerminalClient = class {
|
|
|
228
301
|
);
|
|
229
302
|
}
|
|
230
303
|
/**
|
|
231
|
-
* Kill the terminal session
|
|
304
|
+
* Kill the terminal session (close and terminate)
|
|
232
305
|
*/
|
|
233
306
|
kill() {
|
|
234
307
|
if (!this.ws || this.state !== "connected") {
|
|
@@ -247,6 +320,91 @@ var TerminalClient = class {
|
|
|
247
320
|
this.sessionInfo = null;
|
|
248
321
|
}
|
|
249
322
|
// ==========================================
|
|
323
|
+
// Session Multiplexing Methods
|
|
324
|
+
// ==========================================
|
|
325
|
+
/**
|
|
326
|
+
* List available sessions
|
|
327
|
+
*/
|
|
328
|
+
listSessions(filter) {
|
|
329
|
+
return new Promise((resolve, reject) => {
|
|
330
|
+
if (this.state !== "connected" || !this.ws) {
|
|
331
|
+
reject(new Error("Not connected to server"));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.listSessionsResolve = resolve;
|
|
335
|
+
this.ws.send(
|
|
336
|
+
JSON.stringify({
|
|
337
|
+
type: "listSessions",
|
|
338
|
+
filter
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Join an existing session
|
|
345
|
+
*/
|
|
346
|
+
join(options) {
|
|
347
|
+
return new Promise((resolve, reject) => {
|
|
348
|
+
if (this.state !== "connected" || !this.ws) {
|
|
349
|
+
reject(new Error("Not connected to server"));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (this.sessionId) {
|
|
353
|
+
reject(new Error("Already in a session. Call leave() first."));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this.joinResolve = resolve;
|
|
357
|
+
this.joinReject = reject;
|
|
358
|
+
this.ws.send(
|
|
359
|
+
JSON.stringify({
|
|
360
|
+
type: "join",
|
|
361
|
+
options
|
|
362
|
+
})
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Leave the current session without killing it
|
|
368
|
+
*/
|
|
369
|
+
leave(sessionId) {
|
|
370
|
+
if (!this.ws || this.state !== "connected") {
|
|
371
|
+
console.error("[x-shell] Cannot leave: not connected");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const targetSession = sessionId || this.sessionId;
|
|
375
|
+
if (!targetSession) {
|
|
376
|
+
console.error("[x-shell] Cannot leave: no active session");
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
this.ws.send(
|
|
380
|
+
JSON.stringify({
|
|
381
|
+
type: "leave",
|
|
382
|
+
sessionId: targetSession
|
|
383
|
+
})
|
|
384
|
+
);
|
|
385
|
+
if (targetSession === this.sessionId) {
|
|
386
|
+
this.sessionId = null;
|
|
387
|
+
this.sessionInfo = null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Request session list and trigger onSessionList handlers
|
|
392
|
+
* (Fire-and-forget version of listSessions)
|
|
393
|
+
*/
|
|
394
|
+
requestSessionList(filter) {
|
|
395
|
+
this.listSessions(filter).then((sessions) => {
|
|
396
|
+
this.sessionListHandlers.forEach((handler) => {
|
|
397
|
+
try {
|
|
398
|
+
handler(sessions);
|
|
399
|
+
} catch (e) {
|
|
400
|
+
console.error("[x-shell] Error in sessionList handler:", e);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}).catch((err) => {
|
|
404
|
+
console.error("[x-shell] Failed to list sessions:", err);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
// ==========================================
|
|
250
408
|
// Event handlers
|
|
251
409
|
// ==========================================
|
|
252
410
|
/**
|
|
@@ -300,6 +458,42 @@ var TerminalClient = class {
|
|
|
300
458
|
onContainerList(handler) {
|
|
301
459
|
this.containerListHandlers.push(handler);
|
|
302
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Called when session list is received
|
|
463
|
+
*/
|
|
464
|
+
onSessionList(handler) {
|
|
465
|
+
this.sessionListHandlers.push(handler);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Called when successfully joined a session
|
|
469
|
+
*/
|
|
470
|
+
onJoined(handler) {
|
|
471
|
+
this.joinedHandlers.push(handler);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Called when left a session
|
|
475
|
+
*/
|
|
476
|
+
onLeft(handler) {
|
|
477
|
+
this.leftHandlers.push(handler);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Called when another client joins the current session
|
|
481
|
+
*/
|
|
482
|
+
onClientJoined(handler) {
|
|
483
|
+
this.clientJoinedHandlers.push(handler);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Called when another client leaves the current session
|
|
487
|
+
*/
|
|
488
|
+
onClientLeft(handler) {
|
|
489
|
+
this.clientLeftHandlers.push(handler);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Called when the session is closed by owner or orphan timeout
|
|
493
|
+
*/
|
|
494
|
+
onSessionClosed(handler) {
|
|
495
|
+
this.sessionClosedHandlers.push(handler);
|
|
496
|
+
}
|
|
303
497
|
/**
|
|
304
498
|
* Request list of available containers
|
|
305
499
|
*/
|