@dmsdc-ai/aigentry-telepty 0.1.4 → 0.1.6
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/.claude/commands/telepty-allow.md +58 -0
- package/.claude/commands/telepty-attach.md +22 -0
- package/.claude/commands/telepty-inject.md +34 -0
- package/.claude/commands/telepty-list.md +22 -0
- package/.claude/commands/telepty-manual-test.md +73 -0
- package/.claude/commands/telepty-start.md +25 -0
- package/.claude/commands/telepty-test.md +25 -0
- package/.claude/commands/telepty.md +82 -0
- package/.gemini/skills/telepty/SKILL.md +19 -1
- package/.github/workflows/test-install.yml +18 -24
- package/README.md +16 -0
- package/cli.js +267 -46
- package/daemon.js +261 -27
- package/package.json +4 -2
- package/test/auth.test.js +56 -0
- package/test/cli.test.js +57 -0
- package/test/daemon.test.js +415 -0
- package/test-support/daemon-harness.js +313 -0
package/daemon.js
CHANGED
|
@@ -35,9 +35,34 @@ const PORT = process.env.PORT || 3848;
|
|
|
35
35
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
36
36
|
|
|
37
37
|
const sessions = {};
|
|
38
|
+
const STRIPPED_SESSION_ENV_KEYS = [
|
|
39
|
+
'CLAUDECODE',
|
|
40
|
+
'CODEX_CI',
|
|
41
|
+
'CODEX_THREAD_ID'
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function buildSessionEnv(sessionId) {
|
|
45
|
+
const env = {
|
|
46
|
+
...process.env,
|
|
47
|
+
TERM: os.platform() === 'win32' ? undefined : 'xterm-256color',
|
|
48
|
+
TELEPTY_SESSION_ID: sessionId
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
for (const key of STRIPPED_SESSION_ENV_KEYS) {
|
|
52
|
+
delete env[key];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const key of Object.keys(env)) {
|
|
56
|
+
if (key.startsWith('CLAUDECODE_')) {
|
|
57
|
+
delete env[key];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return env;
|
|
62
|
+
}
|
|
38
63
|
|
|
39
64
|
app.post('/api/sessions/spawn', (req, res) => {
|
|
40
|
-
const { session_id, command, args = [], cwd = process.cwd(), cols = 80, rows = 30 } = req.body;
|
|
65
|
+
const { session_id, command, args = [], cwd = process.cwd(), cols = 80, rows = 30, type = 'AGENT' } = req.body;
|
|
41
66
|
if (!session_id) return res.status(400).json({ error: 'session_id is strictly required.' });
|
|
42
67
|
if (sessions[session_id]) return res.status(409).json({ error: `Session ID '${session_id}' is already active.` });
|
|
43
68
|
if (!command) return res.status(400).json({ error: 'command is required' });
|
|
@@ -49,15 +74,18 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
49
74
|
try {
|
|
50
75
|
console.log(`[SPAWN] Spawning ${shell} with args:`, shellArgs, "in cwd:", cwd);
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
// For bash/zsh, we can inject a custom PS1 variable.
|
|
54
|
-
let customEnv = { ...process.env, TERM: isWin ? undefined : 'xterm-256color', TELEPTY_SESSION_ID: session_id };
|
|
77
|
+
const customEnv = buildSessionEnv(session_id);
|
|
55
78
|
|
|
56
79
|
if (!isWin) {
|
|
80
|
+
const label = type.toUpperCase();
|
|
81
|
+
const colorCode = label === 'USER' ? '32' : '35'; // USER: Green (32), AGENT: Magenta (35)
|
|
82
|
+
const zshColor = label === 'USER' ? 'green' : 'magenta';
|
|
83
|
+
|
|
57
84
|
if (command.includes('bash')) {
|
|
58
|
-
customEnv.PS1 = `\\[\\e[
|
|
85
|
+
customEnv.PS1 = `\\[\\e[${colorCode}m\\][${label}: ${session_id}]\\[\\e[0m\\] \\w \\$ `;
|
|
59
86
|
} else if (command.includes('zsh')) {
|
|
60
|
-
customEnv.
|
|
87
|
+
customEnv.DISABLE_AUTO_TITLE = 'true';
|
|
88
|
+
customEnv.PROMPT = `%F{${zshColor}}[${label}: ${session_id}]%f %~ %# `;
|
|
61
89
|
}
|
|
62
90
|
}
|
|
63
91
|
|
|
@@ -69,24 +97,51 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
69
97
|
env: customEnv
|
|
70
98
|
});
|
|
71
99
|
|
|
72
|
-
|
|
100
|
+
const sessionRecord = {
|
|
101
|
+
id: session_id,
|
|
102
|
+
type: 'spawned',
|
|
73
103
|
ptyProcess,
|
|
74
104
|
command,
|
|
75
105
|
cwd,
|
|
76
106
|
createdAt: new Date().toISOString(),
|
|
77
|
-
clients: new Set()
|
|
107
|
+
clients: new Set(),
|
|
108
|
+
isClosing: false
|
|
78
109
|
};
|
|
110
|
+
sessions[session_id] = sessionRecord;
|
|
111
|
+
|
|
112
|
+
// Broadcast session creation to bus
|
|
113
|
+
const spawnMsg = JSON.stringify({
|
|
114
|
+
type: 'session_spawn',
|
|
115
|
+
sender: 'daemon',
|
|
116
|
+
session_id,
|
|
117
|
+
command,
|
|
118
|
+
cwd,
|
|
119
|
+
timestamp: new Date().toISOString()
|
|
120
|
+
});
|
|
121
|
+
busClients.forEach(client => {
|
|
122
|
+
if (client.readyState === 1) client.send(spawnMsg);
|
|
123
|
+
});
|
|
79
124
|
|
|
80
125
|
ptyProcess.onData((data) => {
|
|
81
|
-
sessions[
|
|
126
|
+
const currentSession = sessions[sessionRecord.id];
|
|
127
|
+
if (!currentSession || currentSession !== sessionRecord) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Send to direct WS clients
|
|
132
|
+
currentSession.clients.forEach(ws => {
|
|
82
133
|
if (ws.readyState === 1) ws.send(JSON.stringify({ type: 'output', data }));
|
|
83
134
|
});
|
|
84
135
|
});
|
|
85
136
|
|
|
86
137
|
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
138
|
+
const currentId = sessionRecord.id;
|
|
139
|
+
console.log(`[EXIT] Session ${currentId} exited with code ${exitCode}`);
|
|
140
|
+
sessionRecord.isClosing = true;
|
|
141
|
+
sessionRecord.clients.forEach(ws => ws.close(1000, 'Session exited'));
|
|
142
|
+
if (sessions[currentId] === sessionRecord) {
|
|
143
|
+
delete sessions[currentId];
|
|
144
|
+
}
|
|
90
145
|
});
|
|
91
146
|
|
|
92
147
|
console.log(`[SPAWN] Created session ${session_id} (${command})`);
|
|
@@ -96,9 +151,44 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
96
151
|
}
|
|
97
152
|
});
|
|
98
153
|
|
|
154
|
+
app.post('/api/sessions/register', (req, res) => {
|
|
155
|
+
const { session_id, command, cwd = process.cwd() } = req.body;
|
|
156
|
+
if (!session_id) return res.status(400).json({ error: 'session_id is required' });
|
|
157
|
+
if (sessions[session_id]) return res.status(409).json({ error: `Session ID '${session_id}' is already active.` });
|
|
158
|
+
|
|
159
|
+
const sessionRecord = {
|
|
160
|
+
id: session_id,
|
|
161
|
+
type: 'wrapped',
|
|
162
|
+
ptyProcess: null,
|
|
163
|
+
ownerWs: null,
|
|
164
|
+
command: command || 'wrapped',
|
|
165
|
+
cwd,
|
|
166
|
+
createdAt: new Date().toISOString(),
|
|
167
|
+
clients: new Set(),
|
|
168
|
+
isClosing: false
|
|
169
|
+
};
|
|
170
|
+
sessions[session_id] = sessionRecord;
|
|
171
|
+
|
|
172
|
+
const busMsg = JSON.stringify({
|
|
173
|
+
type: 'session_register',
|
|
174
|
+
sender: 'daemon',
|
|
175
|
+
session_id,
|
|
176
|
+
command: sessionRecord.command,
|
|
177
|
+
cwd,
|
|
178
|
+
timestamp: new Date().toISOString()
|
|
179
|
+
});
|
|
180
|
+
busClients.forEach(client => {
|
|
181
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
console.log(`[REGISTER] Registered wrapped session ${session_id}`);
|
|
185
|
+
res.status(201).json({ session_id, type: 'wrapped', command: sessionRecord.command, cwd });
|
|
186
|
+
});
|
|
187
|
+
|
|
99
188
|
app.get('/api/sessions', (req, res) => {
|
|
100
189
|
const list = Object.entries(sessions).map(([id, session]) => ({
|
|
101
190
|
id,
|
|
191
|
+
type: session.type || 'spawned',
|
|
102
192
|
command: session.command,
|
|
103
193
|
cwd: session.cwd,
|
|
104
194
|
createdAt: session.createdAt,
|
|
@@ -118,8 +208,30 @@ app.post('/api/sessions/multicast/inject', (req, res) => {
|
|
|
118
208
|
const session = sessions[id];
|
|
119
209
|
if (session) {
|
|
120
210
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
211
|
+
const injectData = `${prompt}\r`;
|
|
212
|
+
if (session.type === 'wrapped') {
|
|
213
|
+
if (session.ownerWs && session.ownerWs.readyState === 1) {
|
|
214
|
+
session.ownerWs.send(JSON.stringify({ type: 'inject', data: injectData }));
|
|
215
|
+
results.successful.push(id);
|
|
216
|
+
} else {
|
|
217
|
+
results.failed.push({ id, error: 'Wrap process not connected' });
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
session.ptyProcess.write(injectData);
|
|
221
|
+
results.successful.push(id);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Broadcast injection to bus
|
|
225
|
+
const busMsg = JSON.stringify({
|
|
226
|
+
type: 'injection',
|
|
227
|
+
sender: 'cli',
|
|
228
|
+
target_agent: id,
|
|
229
|
+
content: prompt,
|
|
230
|
+
timestamp: new Date().toISOString()
|
|
231
|
+
});
|
|
232
|
+
busClients.forEach(client => {
|
|
233
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
234
|
+
});
|
|
123
235
|
} catch (err) {
|
|
124
236
|
results.failed.push({ id, error: err.message });
|
|
125
237
|
}
|
|
@@ -138,14 +250,40 @@ app.post('/api/sessions/broadcast/inject', (req, res) => {
|
|
|
138
250
|
const results = { successful: [], failed: [] };
|
|
139
251
|
|
|
140
252
|
Object.keys(sessions).forEach(id => {
|
|
253
|
+
const session = sessions[id];
|
|
141
254
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
255
|
+
const injectData = `${prompt}\r`;
|
|
256
|
+
if (session.type === 'wrapped') {
|
|
257
|
+
if (session.ownerWs && session.ownerWs.readyState === 1) {
|
|
258
|
+
session.ownerWs.send(JSON.stringify({ type: 'inject', data: injectData }));
|
|
259
|
+
results.successful.push(id);
|
|
260
|
+
} else {
|
|
261
|
+
results.failed.push({ id, error: 'Wrap process not connected' });
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
session.ptyProcess.write(injectData);
|
|
265
|
+
results.successful.push(id);
|
|
266
|
+
}
|
|
144
267
|
} catch (err) {
|
|
145
268
|
results.failed.push({ id, error: err.message });
|
|
146
269
|
}
|
|
147
270
|
});
|
|
148
271
|
|
|
272
|
+
// Send a single bus event for the entire broadcast (not per-session)
|
|
273
|
+
if (results.successful.length > 0) {
|
|
274
|
+
const busMsg = JSON.stringify({
|
|
275
|
+
type: 'injection',
|
|
276
|
+
sender: 'cli',
|
|
277
|
+
target_agent: 'all',
|
|
278
|
+
content: prompt,
|
|
279
|
+
session_ids: results.successful,
|
|
280
|
+
timestamp: new Date().toISOString()
|
|
281
|
+
});
|
|
282
|
+
busClients.forEach(client => {
|
|
283
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
149
287
|
res.json({ success: true, results });
|
|
150
288
|
});
|
|
151
289
|
|
|
@@ -156,23 +294,80 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
156
294
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
157
295
|
if (!prompt) return res.status(400).json({ error: 'prompt is required' });
|
|
158
296
|
try {
|
|
159
|
-
|
|
297
|
+
const injectData = no_enter ? prompt : `${prompt}\r`;
|
|
298
|
+
if (session.type === 'wrapped') {
|
|
299
|
+
if (session.ownerWs && session.ownerWs.readyState === 1) {
|
|
300
|
+
session.ownerWs.send(JSON.stringify({ type: 'inject', data: injectData }));
|
|
301
|
+
} else {
|
|
302
|
+
return res.status(503).json({ error: 'Wrap process is not connected' });
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
session.ptyProcess.write(injectData);
|
|
306
|
+
}
|
|
160
307
|
console.log(`[INJECT] Wrote to session ${id}`);
|
|
308
|
+
|
|
309
|
+
const busMsg = JSON.stringify({
|
|
310
|
+
type: 'injection',
|
|
311
|
+
sender: 'cli',
|
|
312
|
+
target_agent: id,
|
|
313
|
+
content: prompt,
|
|
314
|
+
timestamp: new Date().toISOString()
|
|
315
|
+
});
|
|
316
|
+
busClients.forEach(client => {
|
|
317
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
318
|
+
});
|
|
319
|
+
|
|
161
320
|
res.json({ success: true });
|
|
162
321
|
} catch (err) {
|
|
163
322
|
res.status(500).json({ error: err.message });
|
|
164
323
|
}
|
|
165
324
|
});
|
|
166
325
|
|
|
326
|
+
app.patch('/api/sessions/:id', (req, res) => {
|
|
327
|
+
const { id } = req.params;
|
|
328
|
+
const { new_id } = req.body;
|
|
329
|
+
const session = sessions[id];
|
|
330
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
331
|
+
if (!new_id) return res.status(400).json({ error: 'new_id is required' });
|
|
332
|
+
if (sessions[new_id]) return res.status(409).json({ error: `Session ID '${new_id}' is already in use.` });
|
|
333
|
+
|
|
334
|
+
// Move session to new key
|
|
335
|
+
sessions[new_id] = session;
|
|
336
|
+
delete sessions[id];
|
|
337
|
+
session.id = new_id;
|
|
338
|
+
|
|
339
|
+
// Broadcast rename to bus
|
|
340
|
+
const busMsg = JSON.stringify({
|
|
341
|
+
type: 'session_rename',
|
|
342
|
+
sender: 'daemon',
|
|
343
|
+
old_id: id,
|
|
344
|
+
new_id,
|
|
345
|
+
timestamp: new Date().toISOString()
|
|
346
|
+
});
|
|
347
|
+
busClients.forEach(client => {
|
|
348
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
console.log(`[RENAME] Session '${id}' renamed to '${new_id}'`);
|
|
352
|
+
res.json({ success: true, old_id: id, new_id });
|
|
353
|
+
});
|
|
354
|
+
|
|
167
355
|
app.delete('/api/sessions/:id', (req, res) => {
|
|
168
356
|
const { id } = req.params;
|
|
169
357
|
const session = sessions[id];
|
|
170
358
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
359
|
+
if (session.isClosing) return res.json({ success: true, status: 'closing' });
|
|
171
360
|
try {
|
|
172
|
-
session.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
361
|
+
session.isClosing = true;
|
|
362
|
+
if (session.type === 'wrapped') {
|
|
363
|
+
session.clients.forEach(ws => ws.close(1000, 'Session destroyed'));
|
|
364
|
+
delete sessions[id];
|
|
365
|
+
console.log(`[KILL] Wrapped session ${id} removed`);
|
|
366
|
+
} else {
|
|
367
|
+
session.ptyProcess.kill();
|
|
368
|
+
console.log(`[KILL] Session ${id} forcefully closed`);
|
|
369
|
+
}
|
|
370
|
+
res.json({ success: true, status: 'closing' });
|
|
176
371
|
} catch (err) {
|
|
177
372
|
res.status(500).json({ error: err.message });
|
|
178
373
|
}
|
|
@@ -215,15 +410,44 @@ wss.on('connection', (ws, req) => {
|
|
|
215
410
|
}
|
|
216
411
|
|
|
217
412
|
session.clients.add(ws);
|
|
218
|
-
|
|
413
|
+
|
|
414
|
+
// For wrapped sessions, first connector becomes the owner
|
|
415
|
+
if (session.type === 'wrapped' && !session.ownerWs) {
|
|
416
|
+
session.ownerWs = ws;
|
|
417
|
+
console.log(`[WS] Wrap owner connected for session ${sessionId} (Total: ${session.clients.size})`);
|
|
418
|
+
} else {
|
|
419
|
+
console.log(`[WS] Client attached to session ${sessionId} (Total: ${session.clients.size})`);
|
|
420
|
+
}
|
|
219
421
|
|
|
220
422
|
ws.on('message', (message) => {
|
|
221
423
|
try {
|
|
222
424
|
const { type, data, cols, rows } = JSON.parse(message);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
425
|
+
|
|
426
|
+
if (session.type === 'wrapped') {
|
|
427
|
+
if (ws === session.ownerWs) {
|
|
428
|
+
// Owner sending output -> broadcast to other clients
|
|
429
|
+
if (type === 'output') {
|
|
430
|
+
session.clients.forEach(client => {
|
|
431
|
+
if (client !== ws && client.readyState === 1) {
|
|
432
|
+
client.send(JSON.stringify({ type: 'output', data }));
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
// Non-owner client input -> forward to owner as inject
|
|
438
|
+
if (type === 'input' && session.ownerWs && session.ownerWs.readyState === 1) {
|
|
439
|
+
session.ownerWs.send(JSON.stringify({ type: 'inject', data }));
|
|
440
|
+
} else if (type === 'resize' && session.ownerWs && session.ownerWs.readyState === 1) {
|
|
441
|
+
session.ownerWs.send(JSON.stringify({ type: 'resize', cols, rows }));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
// Existing spawned session logic
|
|
446
|
+
if (type === 'input') {
|
|
447
|
+
session.ptyProcess.write(data);
|
|
448
|
+
} else if (type === 'resize') {
|
|
449
|
+
session.ptyProcess.resize(cols, rows);
|
|
450
|
+
}
|
|
227
451
|
}
|
|
228
452
|
} catch (e) {
|
|
229
453
|
console.error('[WS] Invalid message format', e);
|
|
@@ -232,7 +456,17 @@ wss.on('connection', (ws, req) => {
|
|
|
232
456
|
|
|
233
457
|
ws.on('close', () => {
|
|
234
458
|
session.clients.delete(ws);
|
|
235
|
-
|
|
459
|
+
if (session.type === 'wrapped' && ws === session.ownerWs) {
|
|
460
|
+
session.ownerWs = null;
|
|
461
|
+
console.log(`[WS] Wrap owner disconnected from session ${sessionId} (Total: ${session.clients.size})`);
|
|
462
|
+
// Clean up wrapped session when owner disconnects and no other clients
|
|
463
|
+
if (session.clients.size === 0 && !session.isClosing) {
|
|
464
|
+
delete sessions[sessionId];
|
|
465
|
+
console.log(`[CLEANUP] Wrapped session ${sessionId} removed (owner disconnected)`);
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
console.log(`[WS] Client detached from session ${sessionId} (Total: ${session.clients.size})`);
|
|
469
|
+
}
|
|
236
470
|
});
|
|
237
471
|
});
|
|
238
472
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmsdc-ai/aigentry-telepty",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"main": "daemon.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"telepty": "cli.js",
|
|
7
7
|
"telepty-install": "install.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "node --test test/auth.test.js test/daemon.test.js test/cli.test.js",
|
|
11
|
+
"test:watch": "node --test --watch test/auth.test.js test/daemon.test.js test/cli.test.js",
|
|
12
|
+
"test:ci": "node --test --test-reporter=spec test/auth.test.js test/daemon.test.js test/cli.test.js"
|
|
11
13
|
},
|
|
12
14
|
"keywords": [],
|
|
13
15
|
"author": "",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test, before, after } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// Use a temp directory to isolate tests from the real ~/.telepty config.
|
|
10
|
+
// We patch os.homedir() before requiring auth.js so the module-level
|
|
11
|
+
// constants (CONFIG_DIR, CONFIG_FILE) resolve to the temp dir.
|
|
12
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'telepty-test-'));
|
|
13
|
+
const originalHomedir = os.homedir.bind(os);
|
|
14
|
+
|
|
15
|
+
before(() => {
|
|
16
|
+
os.homedir = () => tmpDir;
|
|
17
|
+
// Ensure auth.js is loaded fresh with the patched homedir
|
|
18
|
+
delete require.cache[require.resolve('../auth.js')];
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
after(() => {
|
|
22
|
+
os.homedir = originalHomedir;
|
|
23
|
+
delete require.cache[require.resolve('../auth.js')];
|
|
24
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('getConfig() returns an object with authToken property', () => {
|
|
28
|
+
const { getConfig } = require('../auth.js');
|
|
29
|
+
const config = getConfig();
|
|
30
|
+
assert.ok(config !== null && typeof config === 'object', 'config should be an object');
|
|
31
|
+
assert.ok('authToken' in config, 'config should have authToken property');
|
|
32
|
+
assert.equal(typeof config.authToken, 'string', 'authToken should be a string');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('authToken is a valid UUID v4 format', () => {
|
|
36
|
+
const { getConfig } = require('../auth.js');
|
|
37
|
+
const config = getConfig();
|
|
38
|
+
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
39
|
+
assert.match(config.authToken, uuidV4Regex, 'authToken should be a valid UUID v4');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('config object has createdAt field', () => {
|
|
43
|
+
const { getConfig } = require('../auth.js');
|
|
44
|
+
const config = getConfig();
|
|
45
|
+
assert.ok('createdAt' in config, 'config should have createdAt property');
|
|
46
|
+
const date = new Date(config.createdAt);
|
|
47
|
+
assert.ok(!isNaN(date.getTime()), 'createdAt should be a valid ISO date string');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('calling getConfig() twice returns the same token (persistence)', () => {
|
|
51
|
+
const { getConfig } = require('../auth.js');
|
|
52
|
+
const config1 = getConfig();
|
|
53
|
+
const config2 = getConfig();
|
|
54
|
+
assert.equal(config1.authToken, config2.authToken, 'authToken should be identical across calls');
|
|
55
|
+
assert.equal(config1.createdAt, config2.createdAt, 'createdAt should be identical across calls');
|
|
56
|
+
});
|
package/test/cli.test.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { afterEach, beforeEach, test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { createSessionId, startTestDaemon, stripAnsi, waitFor } = require('../test-support/daemon-harness');
|
|
6
|
+
|
|
7
|
+
let harness;
|
|
8
|
+
|
|
9
|
+
function collectJsonMessages(ws) {
|
|
10
|
+
const messages = [];
|
|
11
|
+
ws.on('message', (chunk) => {
|
|
12
|
+
try {
|
|
13
|
+
messages.push(JSON.parse(chunk.toString()));
|
|
14
|
+
} catch {
|
|
15
|
+
// Ignore malformed payloads in tests.
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return messages;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
harness = await startTestDaemon();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
await harness.stop();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('telepty list prints active sessions from the configured host and port', async () => {
|
|
30
|
+
const sessionId = createSessionId('cli-list');
|
|
31
|
+
await harness.spawnSession(sessionId);
|
|
32
|
+
|
|
33
|
+
const result = await harness.runCli(['list']);
|
|
34
|
+
assert.equal(result.code, 0, result.stderr);
|
|
35
|
+
|
|
36
|
+
const output = stripAnsi(`${result.stdout}\n${result.stderr}`);
|
|
37
|
+
assert.match(output, new RegExp(sessionId));
|
|
38
|
+
assert.match(output, /Active Sessions/i);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('telepty inject forwards input to the target PTY session', async () => {
|
|
42
|
+
const sessionId = createSessionId('cli-inject');
|
|
43
|
+
await harness.spawnSession(sessionId);
|
|
44
|
+
|
|
45
|
+
const ws = await harness.connectSession(sessionId);
|
|
46
|
+
const outputs = collectJsonMessages(ws);
|
|
47
|
+
const token = createSessionId('cli-token');
|
|
48
|
+
|
|
49
|
+
const result = await harness.runCli(['inject', sessionId, `echo ${token}`]);
|
|
50
|
+
assert.equal(result.code, 0, result.stderr);
|
|
51
|
+
|
|
52
|
+
await waitFor(() => outputs.some((message) => (
|
|
53
|
+
message.type === 'output' && String(message.data).includes(token)
|
|
54
|
+
)), { timeoutMs: 7000, description: 'CLI inject output' });
|
|
55
|
+
|
|
56
|
+
ws.close();
|
|
57
|
+
});
|