@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
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { once } = require('events');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { spawn } = require('child_process');
|
|
8
|
+
const WebSocket = require('ws');
|
|
9
|
+
|
|
10
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
11
|
+
|
|
12
|
+
let sessionCounter = 0;
|
|
13
|
+
|
|
14
|
+
function delay(ms) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function waitFor(check, options = {}) {
|
|
19
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
20
|
+
const intervalMs = options.intervalMs ?? 50;
|
|
21
|
+
const description = options.description ?? 'condition';
|
|
22
|
+
const deadline = Date.now() + timeoutMs;
|
|
23
|
+
let lastError = null;
|
|
24
|
+
|
|
25
|
+
while (Date.now() < deadline) {
|
|
26
|
+
try {
|
|
27
|
+
const result = await check();
|
|
28
|
+
if (result) {
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
lastError = error;
|
|
33
|
+
}
|
|
34
|
+
await delay(intervalMs);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (lastError) {
|
|
38
|
+
throw lastError;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new Error(`Timed out waiting for ${description}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stripAnsi(value) {
|
|
45
|
+
return value.replace(/\u001b\[[0-9;]*m/g, '').replace(/\u001b\].*?\u0007/g, '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createSessionId(prefix = 'session') {
|
|
49
|
+
sessionCounter += 1;
|
|
50
|
+
return `${prefix}-${process.pid}-${Date.now()}-${sessionCounter}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getShellSpec() {
|
|
54
|
+
if (process.platform === 'win32') {
|
|
55
|
+
return { command: 'powershell', args: ['-NoLogo', '-NoProfile'] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { command: 'bash', args: ['--noprofile', '--norc'] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createTempHome() {
|
|
62
|
+
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'telepty-home-'));
|
|
63
|
+
return {
|
|
64
|
+
homeDir,
|
|
65
|
+
env: process.platform === 'win32'
|
|
66
|
+
? { HOME: homeDir, USERPROFILE: homeDir }
|
|
67
|
+
: { HOME: homeDir }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function randomPort() {
|
|
72
|
+
return 30000 + Math.floor(Math.random() * 20000);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function parseResponse(response) {
|
|
76
|
+
const text = await response.text();
|
|
77
|
+
let body = null;
|
|
78
|
+
|
|
79
|
+
if (text) {
|
|
80
|
+
try {
|
|
81
|
+
body = JSON.parse(text);
|
|
82
|
+
} catch {
|
|
83
|
+
body = text;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
response,
|
|
89
|
+
status: response.status,
|
|
90
|
+
headers: response.headers,
|
|
91
|
+
text,
|
|
92
|
+
body
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function startTestDaemon(options = {}) {
|
|
97
|
+
const port = options.port ?? randomPort();
|
|
98
|
+
const host = '127.0.0.1';
|
|
99
|
+
const { homeDir, env: homeEnv } = createTempHome();
|
|
100
|
+
const sharedEnv = {
|
|
101
|
+
...process.env,
|
|
102
|
+
...homeEnv,
|
|
103
|
+
...(options.env || {}),
|
|
104
|
+
NO_UPDATE_NOTIFIER: '1',
|
|
105
|
+
TELEPTY_DISABLE_UPDATE_NOTIFIER: '1'
|
|
106
|
+
};
|
|
107
|
+
const daemonEnv = {
|
|
108
|
+
...sharedEnv,
|
|
109
|
+
PORT: String(port),
|
|
110
|
+
HOST: host
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
let stdout = '';
|
|
114
|
+
let stderr = '';
|
|
115
|
+
|
|
116
|
+
const child = spawn(process.execPath, ['daemon.js'], {
|
|
117
|
+
cwd: projectRoot,
|
|
118
|
+
env: daemonEnv,
|
|
119
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
child.stdout.on('data', (chunk) => {
|
|
123
|
+
stdout += chunk.toString();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
child.stderr.on('data', (chunk) => {
|
|
127
|
+
stderr += chunk.toString();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
async function request(pathname, options = {}) {
|
|
131
|
+
const headers = { ...(options.headers || {}) };
|
|
132
|
+
const init = {
|
|
133
|
+
method: options.method || 'GET',
|
|
134
|
+
headers
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (options.body !== undefined) {
|
|
138
|
+
headers['Content-Type'] = 'application/json';
|
|
139
|
+
init.body = JSON.stringify(options.body);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const response = await fetch(`http://${host}:${port}${pathname}`, init);
|
|
143
|
+
return parseResponse(response);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await waitFor(async () => {
|
|
147
|
+
if (child.exitCode !== null) {
|
|
148
|
+
throw new Error(`Daemon exited early.\nstdout:\n${stdout}\nstderr:\n${stderr}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch(`http://${host}:${port}/api/sessions`);
|
|
153
|
+
return response.ok;
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}, { timeoutMs: 7000, description: 'daemon start' });
|
|
158
|
+
|
|
159
|
+
async function cleanupSessions() {
|
|
160
|
+
const list = await request('/api/sessions');
|
|
161
|
+
if (list.status !== 200 || !Array.isArray(list.body)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await Promise.all(list.body.map((session) => request(`/api/sessions/${encodeURIComponent(session.id)}`, {
|
|
166
|
+
method: 'DELETE'
|
|
167
|
+
})));
|
|
168
|
+
|
|
169
|
+
await waitFor(async () => {
|
|
170
|
+
const current = await request('/api/sessions');
|
|
171
|
+
return current.status === 200 && Array.isArray(current.body) && current.body.length === 0;
|
|
172
|
+
}, { description: 'session cleanup' });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function stop() {
|
|
176
|
+
try {
|
|
177
|
+
await cleanupSessions();
|
|
178
|
+
} catch {
|
|
179
|
+
// Ignore cleanup failures during shutdown and force-stop the daemon below.
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (child.exitCode === null) {
|
|
183
|
+
child.kill();
|
|
184
|
+
|
|
185
|
+
const exited = await Promise.race([
|
|
186
|
+
once(child, 'exit').then(() => true),
|
|
187
|
+
delay(2000).then(() => false)
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
if (!exited && child.exitCode === null) {
|
|
191
|
+
child.kill('SIGKILL');
|
|
192
|
+
await once(child, 'exit').catch(() => {});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function spawnSession(sessionId, overrides = {}) {
|
|
200
|
+
const body = {
|
|
201
|
+
session_id: sessionId,
|
|
202
|
+
cwd: projectRoot,
|
|
203
|
+
cols: 80,
|
|
204
|
+
rows: 24,
|
|
205
|
+
type: 'USER',
|
|
206
|
+
...getShellSpec(),
|
|
207
|
+
...overrides
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return request('/api/sessions/spawn', { method: 'POST', body });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function registerSession(sessionId, overrides = {}) {
|
|
214
|
+
const body = {
|
|
215
|
+
session_id: sessionId,
|
|
216
|
+
command: 'test-wrap',
|
|
217
|
+
cwd: projectRoot,
|
|
218
|
+
...overrides
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
return request('/api/sessions/register', { method: 'POST', body });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function connectWebSocket(pathname) {
|
|
225
|
+
const ws = new WebSocket(`ws://${host}:${port}${pathname}`);
|
|
226
|
+
await new Promise((resolve, reject) => {
|
|
227
|
+
ws.once('open', resolve);
|
|
228
|
+
ws.once('error', reject);
|
|
229
|
+
});
|
|
230
|
+
return ws;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function runCli(args, options = {}) {
|
|
234
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
235
|
+
const cliEnv = {
|
|
236
|
+
...sharedEnv,
|
|
237
|
+
TELEPTY_HOST: host,
|
|
238
|
+
TELEPTY_PORT: String(port),
|
|
239
|
+
...(options.env || {})
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
const cli = spawn(process.execPath, ['cli.js', ...args], {
|
|
244
|
+
cwd: projectRoot,
|
|
245
|
+
env: cliEnv,
|
|
246
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
let cliStdout = '';
|
|
250
|
+
let cliStderr = '';
|
|
251
|
+
let timedOut = false;
|
|
252
|
+
|
|
253
|
+
const timer = setTimeout(() => {
|
|
254
|
+
timedOut = true;
|
|
255
|
+
cli.kill('SIGKILL');
|
|
256
|
+
}, timeoutMs);
|
|
257
|
+
|
|
258
|
+
cli.stdout.on('data', (chunk) => {
|
|
259
|
+
cliStdout += chunk.toString();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
cli.stderr.on('data', (chunk) => {
|
|
263
|
+
cliStderr += chunk.toString();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
cli.on('error', (error) => {
|
|
267
|
+
clearTimeout(timer);
|
|
268
|
+
reject(error);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
cli.on('close', (code, signal) => {
|
|
272
|
+
clearTimeout(timer);
|
|
273
|
+
|
|
274
|
+
if (timedOut) {
|
|
275
|
+
reject(new Error(`CLI command timed out.\nstdout:\n${cliStdout}\nstderr:\n${cliStderr}`));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
resolve({
|
|
280
|
+
code,
|
|
281
|
+
signal,
|
|
282
|
+
stdout: cliStdout,
|
|
283
|
+
stderr: cliStderr
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
port,
|
|
291
|
+
host,
|
|
292
|
+
homeDir,
|
|
293
|
+
request,
|
|
294
|
+
spawnSession,
|
|
295
|
+
registerSession,
|
|
296
|
+
cleanupSessions,
|
|
297
|
+
connectBus: () => connectWebSocket('/api/bus'),
|
|
298
|
+
connectSession: (sessionId) => connectWebSocket(`/api/sessions/${encodeURIComponent(sessionId)}`),
|
|
299
|
+
runCli,
|
|
300
|
+
stop,
|
|
301
|
+
waitFor,
|
|
302
|
+
isAlive: () => child.exitCode === null,
|
|
303
|
+
getLogs: () => ({ stdout, stderr })
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = {
|
|
308
|
+
createSessionId,
|
|
309
|
+
delay,
|
|
310
|
+
startTestDaemon,
|
|
311
|
+
stripAnsi,
|
|
312
|
+
waitFor
|
|
313
|
+
};
|