@jhebe/web-terminal 0.2.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.
@@ -0,0 +1,199 @@
1
+ export function parseClientMessage(value) {
2
+ const message = parseRecord(value);
3
+ if (!message || typeof message.type !== 'string') {
4
+ return undefined;
5
+ }
6
+ switch (message.type) {
7
+ case 'open':
8
+ return hasRequestId(message) &&
9
+ (message.name === undefined || typeof message.name === 'string')
10
+ ? {
11
+ type: 'open',
12
+ requestId: message.requestId,
13
+ ...(message.name === undefined ? {} : { name: message.name }),
14
+ }
15
+ : undefined;
16
+ case 'close':
17
+ case 'attach':
18
+ case 'observe':
19
+ case 'unobserve':
20
+ return hasRequestId(message) && typeof message.sessionId === 'string'
21
+ ? {
22
+ type: message.type,
23
+ requestId: message.requestId,
24
+ sessionId: message.sessionId,
25
+ }
26
+ : undefined;
27
+ case 'detach':
28
+ return hasRequestId(message) && typeof message.sessionId === 'string'
29
+ ? {
30
+ type: 'detach',
31
+ requestId: message.requestId,
32
+ sessionId: message.sessionId,
33
+ }
34
+ : undefined;
35
+ case 'write':
36
+ return hasRequestId(message) &&
37
+ typeof message.sessionId === 'string' &&
38
+ typeof message.data === 'string'
39
+ ? {
40
+ type: 'write',
41
+ requestId: message.requestId,
42
+ sessionId: message.sessionId,
43
+ data: message.data,
44
+ }
45
+ : undefined;
46
+ case 'input':
47
+ return typeof message.sessionId === 'string' &&
48
+ typeof message.data === 'string'
49
+ ? {
50
+ type: 'input',
51
+ sessionId: message.sessionId,
52
+ data: message.data,
53
+ }
54
+ : undefined;
55
+ case 'resize':
56
+ return typeof message.sessionId === 'string' &&
57
+ validDimension(message.cols) &&
58
+ validDimension(message.rows)
59
+ ? {
60
+ type: 'resize',
61
+ sessionId: message.sessionId,
62
+ cols: message.cols,
63
+ rows: message.rows,
64
+ }
65
+ : undefined;
66
+ default:
67
+ return undefined;
68
+ }
69
+ }
70
+ export function parseServerMessage(value) {
71
+ const message = parseRecord(value);
72
+ if (!message || typeof message.type !== 'string') {
73
+ return undefined;
74
+ }
75
+ switch (message.type) {
76
+ case 'sessions':
77
+ return Array.isArray(message.sessions) &&
78
+ message.sessions.every(isSessionSummary)
79
+ ? { type: 'sessions', sessions: message.sessions }
80
+ : undefined;
81
+ case 'opened':
82
+ return hasRequestId(message) && isSessionSummary(message.session)
83
+ ? {
84
+ type: 'opened',
85
+ requestId: message.requestId,
86
+ session: message.session,
87
+ }
88
+ : undefined;
89
+ case 'closed':
90
+ case 'written':
91
+ case 'unobserved':
92
+ return hasRequestId(message) && typeof message.sessionId === 'string'
93
+ ? {
94
+ type: message.type,
95
+ requestId: message.requestId,
96
+ sessionId: message.sessionId,
97
+ }
98
+ : undefined;
99
+ case 'observing':
100
+ return hasRequestId(message) &&
101
+ isSessionSummary(message.session) &&
102
+ typeof message.history === 'string'
103
+ ? {
104
+ type: 'observing',
105
+ requestId: message.requestId,
106
+ session: message.session,
107
+ history: message.history,
108
+ }
109
+ : undefined;
110
+ case 'attached':
111
+ return hasRequestId(message) &&
112
+ isSessionSummary(message.session) &&
113
+ typeof message.history === 'string'
114
+ ? {
115
+ type: 'attached',
116
+ requestId: message.requestId,
117
+ session: message.session,
118
+ history: message.history,
119
+ }
120
+ : undefined;
121
+ case 'detached':
122
+ return hasRequestId(message) && typeof message.sessionId === 'string'
123
+ ? {
124
+ type: 'detached',
125
+ requestId: message.requestId,
126
+ sessionId: message.sessionId,
127
+ }
128
+ : undefined;
129
+ case 'output':
130
+ return typeof message.sessionId === 'string' &&
131
+ typeof message.data === 'string'
132
+ ? {
133
+ type: 'output',
134
+ sessionId: message.sessionId,
135
+ data: message.data,
136
+ }
137
+ : undefined;
138
+ case 'exit':
139
+ return typeof message.sessionId === 'string' &&
140
+ typeof message.exitCode === 'number' &&
141
+ (message.signal === undefined || typeof message.signal === 'number')
142
+ ? {
143
+ type: 'exit',
144
+ sessionId: message.sessionId,
145
+ exitCode: message.exitCode,
146
+ ...(message.signal === undefined ? {} : { signal: message.signal }),
147
+ }
148
+ : undefined;
149
+ case 'error':
150
+ return typeof message.message === 'string' &&
151
+ (message.requestId === undefined ||
152
+ typeof message.requestId === 'string')
153
+ ? {
154
+ type: 'error',
155
+ message: message.message,
156
+ ...(message.requestId === undefined
157
+ ? {}
158
+ : { requestId: message.requestId }),
159
+ }
160
+ : undefined;
161
+ default:
162
+ return undefined;
163
+ }
164
+ }
165
+ function parseRecord(value) {
166
+ if (typeof value !== 'string') {
167
+ return undefined;
168
+ }
169
+ try {
170
+ const parsed = JSON.parse(value);
171
+ return isRecord(parsed) ? parsed : undefined;
172
+ }
173
+ catch {
174
+ return undefined;
175
+ }
176
+ }
177
+ function isSessionSummary(value) {
178
+ return (isRecord(value) &&
179
+ typeof value.id === 'string' &&
180
+ typeof value.name === 'string' &&
181
+ (value.state === 'running' || value.state === 'exited') &&
182
+ typeof value.attached === 'boolean' &&
183
+ typeof value.observers === 'number' &&
184
+ typeof value.createdAt === 'string' &&
185
+ typeof value.lastActiveAt === 'string' &&
186
+ (value.exitCode === undefined || typeof value.exitCode === 'number'));
187
+ }
188
+ function hasRequestId(value) {
189
+ return typeof value.requestId === 'string';
190
+ }
191
+ function validDimension(value) {
192
+ return (typeof value === 'number' &&
193
+ Number.isInteger(value) &&
194
+ value > 0 &&
195
+ value <= 1000);
196
+ }
197
+ function isRecord(value) {
198
+ return typeof value === 'object' && value !== null;
199
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@jhebe/web-terminal",
3
+ "version": "0.2.0",
4
+ "description": "A framework-agnostic terminal session library for embedding managed terminals in web applications.",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "dist-server",
9
+ "bin/web-terminal.mjs",
10
+ "scripts/prepare-node-pty.mjs"
11
+ ],
12
+ "bin": {
13
+ "web-terminal": "bin/web-terminal.mjs"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/web-terminal.js"
19
+ },
20
+ "./style.css": "./dist/web-terminal.css",
21
+ "./server": {
22
+ "types": "./dist-server/server/index.d.ts",
23
+ "import": "./dist-server/server/index.js"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "postinstall": "node scripts/prepare-node-pty.mjs",
28
+ "clean": "rm -rf dist dist-server",
29
+ "build": "npm run clean && npm run build:client && npm run build:server",
30
+ "build:client": "vite build --config vite.config.ts && tsc -p tsconfig.client.json",
31
+ "build:server": "tsc -p tsconfig.server.json",
32
+ "check": "tsc -p tsconfig.check.json",
33
+ "test": "tsx --test test/*.test.ts"
34
+ },
35
+ "keywords": [
36
+ "terminal",
37
+ "web-terminal",
38
+ "xterm",
39
+ "pty",
40
+ "session"
41
+ ],
42
+ "license": "MIT",
43
+ "dependencies": {
44
+ "@xterm/addon-fit": "^0.11.0",
45
+ "@xterm/xterm": "^6.0.0",
46
+ "node-pty": "^1.1.0",
47
+ "ws": "^8.21.0"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^24.0.0",
51
+ "@types/ws": "^8.18.1",
52
+ "tsx": "^4.23.0",
53
+ "typescript": "^7.0.2",
54
+ "vite": "^8.1.4"
55
+ },
56
+ "engines": {
57
+ "node": ">=20"
58
+ }
59
+ }
@@ -0,0 +1,28 @@
1
+ import { chmod, readdir } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+
5
+ if (process.platform !== 'win32') {
6
+ const require = createRequire(import.meta.url);
7
+ const prebuilds = join(
8
+ dirname(require.resolve('node-pty/package.json')),
9
+ 'prebuilds',
10
+ );
11
+
12
+ for (const platformDirectory of await readdir(prebuilds, {
13
+ withFileTypes: true,
14
+ })) {
15
+ if (!platformDirectory.isDirectory()) {
16
+ continue;
17
+ }
18
+
19
+ const helper = join(prebuilds, platformDirectory.name, 'spawn-helper');
20
+ try {
21
+ await chmod(helper, 0o755);
22
+ } catch (error) {
23
+ if (error?.code !== 'ENOENT') {
24
+ throw error;
25
+ }
26
+ }
27
+ }
28
+ }