-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathclient.ts
More file actions
91 lines (85 loc) · 3.01 KB
/
Copy pathclient.ts
File metadata and controls
91 lines (85 loc) · 3.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Client-instance side of the singleton: when another openmicro already owns
// the port, this instance registers itself and receives forwarded keystrokes
// for its own pty over SSE.
import { logger } from './logger.js'
import { HOST_URL } from './ports.js'
/** True if the process listening on the singleton port is an openmicro host. */
export async function isOpenmicroHost(): Promise<boolean> {
try {
const res = await fetch(`${HOST_URL}/health`, { signal: AbortSignal.timeout(1000) })
const body = (await res.json()) as { app?: string }
return body.app === 'openmicro'
} catch {
return false
}
}
/**
* Report this wrapper's terminal focus change to the host (fire-and-forget).
*
* Args:
* wrapperId (string): This instance's OPENMICRO_INSTANCE_ID.
* focused (boolean): True on focus-in (ESC[I), false on focus-out (ESC[O).
*
* Returns:
* None.
*/
export function reportTerminalFocus(wrapperId: string, focused: boolean): void {
fetch(`${HOST_URL}/focus`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ wrapperId, focused }),
}).catch(() => {
// host gone or not openmicro — focus reports are best-effort
})
}
/**
* Register with the host and stream forwarded keystrokes into `write`.
*
* Args:
* wrapperId (string): This instance's OPENMICRO_INSTANCE_ID, for hook ownership.
* kind (string): Harness kind, so the host classifies this session's hooks correctly.
* write (function): Sink for decoded keystroke bytes (this instance's pty).
*
* Returns:
* Promise<void>: Resolves when the host connection closes (host exited).
*/
export async function runAsClient(
wrapperId: string,
kind: string,
write: (bytes: string) => void,
): Promise<void> {
const registration = await fetch(`${HOST_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cwd: process.cwd(), pid: process.pid, wrapperId, kind }),
})
const { instanceId } = (await registration.json()) as { instanceId: string }
logger.info('running as client instance', { instanceId, kind })
const stream = await fetch(`${HOST_URL}/instance/${instanceId}`)
if (!stream.body) return
const decoder = new TextDecoder()
let buffer = ''
for await (const chunk of stream.body) {
buffer += decoder.decode(chunk as Uint8Array, { stream: true })
let sep
while ((sep = buffer.indexOf('\n\n')) !== -1) {
const frame = buffer.slice(0, sep)
buffer = buffer.slice(sep + 2)
const data = frame
.split('\n')
.filter((l) => l.startsWith('data: '))
.map((l) => l.slice(6))
.join('')
if (!data) continue
try {
const msg = JSON.parse(data) as { type?: string; data?: string }
if (msg.type === 'keys' && msg.data) {
write(Buffer.from(msg.data, 'base64').toString('utf8'))
}
} catch (err) {
logger.warn('client: bad frame from host', err)
}
}
}
logger.info('host connection closed')
}