@brimveyn/aimux 1.2.0 → 1.3.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/README.md +29 -8
- package/package.json +6 -7
- package/src/app-runtime/backend-runtime-events.ts +9 -0
- package/src/app-runtime/side-effects.ts +8 -0
- package/src/config/loader.ts +2 -2
- package/src/config.ts +3 -3
- package/src/daemon/daemon.ts +262 -118
- package/src/daemon/runtime-paths.ts +34 -2
- package/src/index.tsx +11 -3
- package/src/ipc/manager-protocol.ts +347 -0
- package/src/ipc/protocol.ts +55 -3
- package/src/platform/daemon-control.ts +42 -11
- package/src/profile-paths.ts +27 -0
- package/src/pty/pty-manager.ts +24 -0
- package/src/restart-daemon.ts +10 -10
- package/src/session-backend/bootstrap.ts +197 -46
- package/src/session-backend/remote-session-backend.ts +126 -59
- package/src/state/session-catalog.ts +5 -4
- package/src/state/snippet-catalog.ts +4 -4
- package/src/terminal-manager/manager-client.ts +360 -0
- package/src/terminal-manager/terminal-manager.ts +272 -0
- package/src/update.ts +3 -3
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TabSession,
|
|
3
|
+
TerminalModeState,
|
|
4
|
+
TerminalSnapshot,
|
|
5
|
+
WorkspaceSnapshotV1,
|
|
6
|
+
} from '../state/types'
|
|
7
|
+
|
|
8
|
+
import { isWorkspaceSnapshotV1 } from '../state/validation'
|
|
9
|
+
import {
|
|
10
|
+
getProcessVersion,
|
|
11
|
+
IpcProtocolError,
|
|
12
|
+
MessageDecoder,
|
|
13
|
+
negotiateProtocolVersion,
|
|
14
|
+
} from './protocol'
|
|
15
|
+
|
|
16
|
+
export const MANAGER_PROTOCOL_MIN_VERSION = 1
|
|
17
|
+
export const MANAGER_PROTOCOL_VERSION = 1
|
|
18
|
+
|
|
19
|
+
export interface ManagerHelloRequest {
|
|
20
|
+
minVersion: number
|
|
21
|
+
maxVersion: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ManagerHelloResult {
|
|
25
|
+
minVersion: number
|
|
26
|
+
maxVersion: number
|
|
27
|
+
processVersion: string
|
|
28
|
+
selectedVersion: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ManagerAttachRequest {
|
|
32
|
+
protocolVersion: number
|
|
33
|
+
sessionId: string
|
|
34
|
+
cols: number
|
|
35
|
+
rows: number
|
|
36
|
+
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ManagerAttachResult {
|
|
40
|
+
protocolVersion: number
|
|
41
|
+
tabs: TabSession[]
|
|
42
|
+
activeTabId: string | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type ManagerRequest =
|
|
46
|
+
| { id: string; type: 'hello'; payload: ManagerHelloRequest }
|
|
47
|
+
| { id: string; type: 'attachSession'; payload: ManagerAttachRequest }
|
|
48
|
+
| {
|
|
49
|
+
id: string
|
|
50
|
+
type: 'createTab'
|
|
51
|
+
payload: {
|
|
52
|
+
sessionId: string
|
|
53
|
+
tabId: string
|
|
54
|
+
assistant: TabSession['assistant']
|
|
55
|
+
title: string
|
|
56
|
+
command: string
|
|
57
|
+
args?: string[]
|
|
58
|
+
cols: number
|
|
59
|
+
rows: number
|
|
60
|
+
cwd?: string
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
| { id: string; type: 'write'; payload: { sessionId: string; tabId: string; data: string } }
|
|
64
|
+
| { id: string; type: 'resizeClient'; payload: { sessionId: string; cols: number; rows: number } }
|
|
65
|
+
| {
|
|
66
|
+
id: string
|
|
67
|
+
type: 'resizeTab'
|
|
68
|
+
payload: { sessionId: string; tabId: string; cols: number; rows: number }
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
id: string
|
|
72
|
+
type: 'scrollToBottom'
|
|
73
|
+
payload: { sessionId: string; tabId: string }
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
id: string
|
|
77
|
+
type: 'scroll'
|
|
78
|
+
payload: { sessionId: string; tabId: string; deltaLines: number }
|
|
79
|
+
}
|
|
80
|
+
| { id: string; type: 'setActiveTab'; payload: { sessionId: string; tabId: string | null } }
|
|
81
|
+
| { id: string; type: 'closeTab'; payload: { sessionId: string; tabId: string } }
|
|
82
|
+
| { id: string; type: 'disposeSession'; payload: { sessionId: string } }
|
|
83
|
+
| { id: string; type: 'ping'; payload: Record<string, never> }
|
|
84
|
+
|
|
85
|
+
export type ManagerResponse =
|
|
86
|
+
| { id: string; type: 'helloResult'; payload: ManagerHelloResult }
|
|
87
|
+
| { id: string; type: 'ok'; payload: Record<string, never> }
|
|
88
|
+
| { id: string; type: 'attachResult'; payload: ManagerAttachResult }
|
|
89
|
+
| { id: string; type: 'error'; payload: { message: string } }
|
|
90
|
+
|
|
91
|
+
export type ManagerEvent =
|
|
92
|
+
| {
|
|
93
|
+
type: 'tabRender'
|
|
94
|
+
payload: {
|
|
95
|
+
sessionId: string
|
|
96
|
+
tabId: string
|
|
97
|
+
viewport: TerminalSnapshot
|
|
98
|
+
terminalModes: TerminalModeState
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
| { type: 'tabExit'; payload: { sessionId: string; tabId: string; exitCode: number } }
|
|
102
|
+
| { type: 'tabError'; payload: { sessionId: string; tabId: string; message: string } }
|
|
103
|
+
|
|
104
|
+
export type ManagerMessage = ManagerRequest | ManagerResponse | ManagerEvent
|
|
105
|
+
|
|
106
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
107
|
+
return typeof value === 'object' && value !== null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isString(value: unknown): value is string {
|
|
111
|
+
return typeof value === 'string'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isNullableString(value: unknown): value is string | null {
|
|
115
|
+
return value === null || isString(value)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
119
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isStringArray(value: unknown): value is string[] {
|
|
123
|
+
return Array.isArray(value) && value.every(isString)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isTerminalSpan(value: unknown): boolean {
|
|
127
|
+
return (
|
|
128
|
+
isObjectRecord(value) &&
|
|
129
|
+
isString(value.text) &&
|
|
130
|
+
(value.fg === undefined || isString(value.fg)) &&
|
|
131
|
+
(value.bg === undefined || isString(value.bg)) &&
|
|
132
|
+
(value.bold === undefined || typeof value.bold === 'boolean') &&
|
|
133
|
+
(value.italic === undefined || typeof value.italic === 'boolean') &&
|
|
134
|
+
(value.underline === undefined || typeof value.underline === 'boolean') &&
|
|
135
|
+
(value.cursor === undefined || typeof value.cursor === 'boolean')
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isTerminalSnapshot(value: unknown): value is TerminalSnapshot {
|
|
140
|
+
return (
|
|
141
|
+
isObjectRecord(value) &&
|
|
142
|
+
Array.isArray(value.lines) &&
|
|
143
|
+
value.lines.every(
|
|
144
|
+
(line) =>
|
|
145
|
+
isObjectRecord(line) && Array.isArray(line.spans) && line.spans.every(isTerminalSpan)
|
|
146
|
+
) &&
|
|
147
|
+
isFiniteNumber(value.viewportY) &&
|
|
148
|
+
isFiniteNumber(value.baseY) &&
|
|
149
|
+
typeof value.cursorVisible === 'boolean'
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
154
|
+
return (
|
|
155
|
+
isObjectRecord(value) &&
|
|
156
|
+
(value.mouseTrackingMode === 'none' ||
|
|
157
|
+
value.mouseTrackingMode === 'x10' ||
|
|
158
|
+
value.mouseTrackingMode === 'vt200' ||
|
|
159
|
+
value.mouseTrackingMode === 'drag' ||
|
|
160
|
+
value.mouseTrackingMode === 'any') &&
|
|
161
|
+
typeof value.sendFocusMode === 'boolean' &&
|
|
162
|
+
typeof value.alternateScrollMode === 'boolean' &&
|
|
163
|
+
typeof value.isAlternateBuffer === 'boolean' &&
|
|
164
|
+
typeof value.bracketedPasteMode === 'boolean'
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isAttachResult(value: unknown): value is ManagerAttachResult {
|
|
169
|
+
return (
|
|
170
|
+
isObjectRecord(value) &&
|
|
171
|
+
isFiniteNumber(value.protocolVersion) &&
|
|
172
|
+
Array.isArray(value.tabs) &&
|
|
173
|
+
value.tabs.every((tab) => isObjectRecord(tab) && isString(tab.id)) &&
|
|
174
|
+
isNullableString(value.activeTabId)
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function isHelloResult(value: unknown): value is ManagerHelloResult {
|
|
179
|
+
return (
|
|
180
|
+
isObjectRecord(value) &&
|
|
181
|
+
isFiniteNumber(value.minVersion) &&
|
|
182
|
+
isFiniteNumber(value.maxVersion) &&
|
|
183
|
+
isFiniteNumber(value.selectedVersion) &&
|
|
184
|
+
isString(value.processVersion)
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function assert(condition: boolean, message: string): asserts condition {
|
|
189
|
+
if (!condition) {
|
|
190
|
+
throw new IpcProtocolError(message)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function selectManagerProtocolVersion(payload: ManagerHelloRequest): number | null {
|
|
195
|
+
return negotiateProtocolVersion(
|
|
196
|
+
payload.minVersion,
|
|
197
|
+
payload.maxVersion,
|
|
198
|
+
MANAGER_PROTOCOL_MIN_VERSION,
|
|
199
|
+
MANAGER_PROTOCOL_VERSION
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function createManagerHelloResult(selectedVersion: number): ManagerHelloResult {
|
|
204
|
+
return {
|
|
205
|
+
maxVersion: MANAGER_PROTOCOL_VERSION,
|
|
206
|
+
minVersion: MANAGER_PROTOCOL_MIN_VERSION,
|
|
207
|
+
processVersion: getProcessVersion(),
|
|
208
|
+
selectedVersion,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function parseManagerRequest(value: unknown): ManagerRequest {
|
|
213
|
+
assert(isObjectRecord(value), 'IPC message must be an object')
|
|
214
|
+
assert(isString(value.id), 'IPC request id must be a string')
|
|
215
|
+
assert(isString(value.type), 'IPC request type must be a string')
|
|
216
|
+
assert(isObjectRecord(value.payload), 'IPC request payload must be an object')
|
|
217
|
+
|
|
218
|
+
switch (value.type) {
|
|
219
|
+
case 'hello':
|
|
220
|
+
assert(isFiniteNumber(value.payload.minVersion), 'hello.minVersion must be a number')
|
|
221
|
+
assert(isFiniteNumber(value.payload.maxVersion), 'hello.maxVersion must be a number')
|
|
222
|
+
return value as ManagerRequest
|
|
223
|
+
case 'attachSession':
|
|
224
|
+
assert(
|
|
225
|
+
isFiniteNumber(value.payload.protocolVersion),
|
|
226
|
+
'attachSession.protocolVersion must be a number'
|
|
227
|
+
)
|
|
228
|
+
assert(isString(value.payload.sessionId), 'attachSession.sessionId must be a string')
|
|
229
|
+
assert(isFiniteNumber(value.payload.cols), 'attachSession.cols must be a number')
|
|
230
|
+
assert(isFiniteNumber(value.payload.rows), 'attachSession.rows must be a number')
|
|
231
|
+
assert(
|
|
232
|
+
value.payload.workspaceSnapshot === undefined ||
|
|
233
|
+
isWorkspaceSnapshotV1(value.payload.workspaceSnapshot),
|
|
234
|
+
'attachSession.workspaceSnapshot must be a valid workspace snapshot'
|
|
235
|
+
)
|
|
236
|
+
return value as ManagerRequest
|
|
237
|
+
case 'createTab':
|
|
238
|
+
assert(isString(value.payload.sessionId), 'createTab.sessionId must be a string')
|
|
239
|
+
assert(isString(value.payload.tabId), 'createTab.tabId must be a string')
|
|
240
|
+
assert(
|
|
241
|
+
isString(value.payload.assistant) && value.payload.assistant.length > 0,
|
|
242
|
+
'createTab.assistant must be a non-empty string'
|
|
243
|
+
)
|
|
244
|
+
assert(isString(value.payload.title), 'createTab.title must be a string')
|
|
245
|
+
assert(isString(value.payload.command), 'createTab.command must be a string')
|
|
246
|
+
assert(
|
|
247
|
+
value.payload.args === undefined || isStringArray(value.payload.args),
|
|
248
|
+
'createTab.args must be a string array'
|
|
249
|
+
)
|
|
250
|
+
assert(isFiniteNumber(value.payload.cols), 'createTab.cols must be a number')
|
|
251
|
+
assert(isFiniteNumber(value.payload.rows), 'createTab.rows must be a number')
|
|
252
|
+
assert(
|
|
253
|
+
value.payload.cwd === undefined || isString(value.payload.cwd),
|
|
254
|
+
'createTab.cwd must be a string'
|
|
255
|
+
)
|
|
256
|
+
return value as ManagerRequest
|
|
257
|
+
case 'write':
|
|
258
|
+
assert(isString(value.payload.sessionId), 'write.sessionId must be a string')
|
|
259
|
+
assert(isString(value.payload.tabId), 'write.tabId must be a string')
|
|
260
|
+
assert(isString(value.payload.data), 'write.data must be a string')
|
|
261
|
+
return value as ManagerRequest
|
|
262
|
+
case 'resizeClient':
|
|
263
|
+
assert(isString(value.payload.sessionId), 'resizeClient.sessionId must be a string')
|
|
264
|
+
assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
|
|
265
|
+
assert(isFiniteNumber(value.payload.rows), 'resizeClient.rows must be a number')
|
|
266
|
+
return value as ManagerRequest
|
|
267
|
+
case 'resizeTab':
|
|
268
|
+
assert(isString(value.payload.sessionId), 'resizeTab.sessionId must be a string')
|
|
269
|
+
assert(isString(value.payload.tabId), 'resizeTab.tabId must be a string')
|
|
270
|
+
assert(isFiniteNumber(value.payload.cols), 'resizeTab.cols must be a number')
|
|
271
|
+
assert(isFiniteNumber(value.payload.rows), 'resizeTab.rows must be a number')
|
|
272
|
+
return value as ManagerRequest
|
|
273
|
+
case 'scroll':
|
|
274
|
+
assert(isString(value.payload.sessionId), 'scroll.sessionId must be a string')
|
|
275
|
+
assert(isString(value.payload.tabId), 'scroll.tabId must be a string')
|
|
276
|
+
assert(isFiniteNumber(value.payload.deltaLines), 'scroll.deltaLines must be a number')
|
|
277
|
+
return value as ManagerRequest
|
|
278
|
+
case 'scrollToBottom':
|
|
279
|
+
assert(isString(value.payload.sessionId), 'scrollToBottom.sessionId must be a string')
|
|
280
|
+
assert(isString(value.payload.tabId), 'scrollToBottom.tabId must be a string')
|
|
281
|
+
return value as ManagerRequest
|
|
282
|
+
case 'setActiveTab':
|
|
283
|
+
assert(isString(value.payload.sessionId), 'setActiveTab.sessionId must be a string')
|
|
284
|
+
assert(isNullableString(value.payload.tabId), 'setActiveTab.tabId must be a string or null')
|
|
285
|
+
return value as ManagerRequest
|
|
286
|
+
case 'closeTab':
|
|
287
|
+
assert(isString(value.payload.sessionId), 'closeTab.sessionId must be a string')
|
|
288
|
+
assert(isString(value.payload.tabId), 'closeTab.tabId must be a string')
|
|
289
|
+
return value as ManagerRequest
|
|
290
|
+
case 'disposeSession':
|
|
291
|
+
assert(isString(value.payload.sessionId), 'disposeSession.sessionId must be a string')
|
|
292
|
+
return value as ManagerRequest
|
|
293
|
+
case 'ping':
|
|
294
|
+
return value as ManagerRequest
|
|
295
|
+
default:
|
|
296
|
+
throw new IpcProtocolError(`Unknown IPC request type: ${String(value.type)}`)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function parseManagerMessage(value: unknown): ManagerResponse | ManagerEvent {
|
|
301
|
+
assert(isObjectRecord(value), 'IPC message must be an object')
|
|
302
|
+
assert(isString(value.type), 'IPC response type must be a string')
|
|
303
|
+
assert(isObjectRecord(value.payload), 'IPC response payload must be an object')
|
|
304
|
+
|
|
305
|
+
switch (value.type) {
|
|
306
|
+
case 'helloResult':
|
|
307
|
+
assert(isString(value.id), 'helloResult.id must be a string')
|
|
308
|
+
assert(isHelloResult(value.payload), 'helloResult.payload is invalid')
|
|
309
|
+
return value as ManagerResponse
|
|
310
|
+
case 'ok':
|
|
311
|
+
assert(isString(value.id), 'ok.id must be a string')
|
|
312
|
+
return value as ManagerResponse
|
|
313
|
+
case 'attachResult':
|
|
314
|
+
assert(isString(value.id), 'attachResult.id must be a string')
|
|
315
|
+
assert(isAttachResult(value.payload), 'attachResult.payload is invalid')
|
|
316
|
+
return value as ManagerResponse
|
|
317
|
+
case 'error':
|
|
318
|
+
assert(isString(value.id), 'error.id must be a string')
|
|
319
|
+
assert(isString(value.payload.message), 'error.message must be a string')
|
|
320
|
+
return value as ManagerResponse
|
|
321
|
+
case 'tabRender':
|
|
322
|
+
assert(isString(value.payload.sessionId), 'tabRender.sessionId must be a string')
|
|
323
|
+
assert(isString(value.payload.tabId), 'tabRender.tabId must be a string')
|
|
324
|
+
assert(isTerminalSnapshot(value.payload.viewport), 'tabRender.viewport is invalid')
|
|
325
|
+
assert(isTerminalModeState(value.payload.terminalModes), 'tabRender.terminalModes is invalid')
|
|
326
|
+
return value as ManagerEvent
|
|
327
|
+
case 'tabExit':
|
|
328
|
+
assert(isString(value.payload.sessionId), 'tabExit.sessionId must be a string')
|
|
329
|
+
assert(isString(value.payload.tabId), 'tabExit.tabId must be a string')
|
|
330
|
+
assert(isFiniteNumber(value.payload.exitCode), 'tabExit.exitCode must be a number')
|
|
331
|
+
return value as ManagerEvent
|
|
332
|
+
case 'tabError':
|
|
333
|
+
assert(isString(value.payload.sessionId), 'tabError.sessionId must be a string')
|
|
334
|
+
assert(isString(value.payload.tabId), 'tabError.tabId must be a string')
|
|
335
|
+
assert(isString(value.payload.message), 'tabError.message must be a string')
|
|
336
|
+
return value as ManagerEvent
|
|
337
|
+
default:
|
|
338
|
+
throw new IpcProtocolError(`Unknown IPC response type: ${String(value.type)}`)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function encodeManagerMessage(message: ManagerMessage): Buffer {
|
|
343
|
+
const payload = JSON.stringify(message)
|
|
344
|
+
return Buffer.from(`${Buffer.byteLength(payload, 'utf8')}\n${payload}`, 'utf8')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export { MessageDecoder }
|
package/src/ipc/protocol.ts
CHANGED
|
@@ -7,7 +7,20 @@ import type {
|
|
|
7
7
|
|
|
8
8
|
import { isWorkspaceSnapshotV1 } from '../state/validation'
|
|
9
9
|
|
|
10
|
-
export const
|
|
10
|
+
export const IPC_PROTOCOL_MIN_VERSION = 2
|
|
11
|
+
export const IPC_PROTOCOL_VERSION = 2
|
|
12
|
+
|
|
13
|
+
export interface ProtocolHelloRequest {
|
|
14
|
+
minVersion: number
|
|
15
|
+
maxVersion: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ProtocolHelloResult {
|
|
19
|
+
minVersion: number
|
|
20
|
+
maxVersion: number
|
|
21
|
+
processVersion: string
|
|
22
|
+
selectedVersion: number
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
export interface AttachRequest {
|
|
13
26
|
protocolVersion: number
|
|
@@ -24,6 +37,7 @@ export interface AttachResult {
|
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
export type ClientRequest =
|
|
40
|
+
| { id: string; type: 'hello'; payload: ProtocolHelloRequest }
|
|
27
41
|
| { id: string; type: 'attach'; payload: AttachRequest }
|
|
28
42
|
| {
|
|
29
43
|
id: string
|
|
@@ -50,6 +64,7 @@ export type ClientRequest =
|
|
|
50
64
|
| { id: string; type: 'ping'; payload: Record<string, never> }
|
|
51
65
|
|
|
52
66
|
export type ServerResponse =
|
|
67
|
+
| { id: string; type: 'helloResult'; payload: ProtocolHelloResult }
|
|
53
68
|
| { id: string; type: 'ok'; payload: Record<string, never> }
|
|
54
69
|
| { id: string; type: 'attachResult'; payload: AttachResult }
|
|
55
70
|
| { id: string; type: 'error'; payload: { message: string } }
|
|
@@ -143,6 +158,16 @@ function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
|
143
158
|
)
|
|
144
159
|
}
|
|
145
160
|
|
|
161
|
+
function isProtocolHelloResult(value: unknown): value is ProtocolHelloResult {
|
|
162
|
+
return (
|
|
163
|
+
isObjectRecord(value) &&
|
|
164
|
+
isFiniteNumber(value.minVersion) &&
|
|
165
|
+
isFiniteNumber(value.maxVersion) &&
|
|
166
|
+
isFiniteNumber(value.selectedVersion) &&
|
|
167
|
+
isString(value.processVersion)
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
146
171
|
function isAttachResult(value: unknown): value is AttachResult {
|
|
147
172
|
return (
|
|
148
173
|
isObjectRecord(value) &&
|
|
@@ -178,6 +203,25 @@ function assert(condition: boolean, message: string): asserts condition {
|
|
|
178
203
|
}
|
|
179
204
|
}
|
|
180
205
|
|
|
206
|
+
export function negotiateProtocolVersion(
|
|
207
|
+
clientMinVersion: number,
|
|
208
|
+
clientMaxVersion: number,
|
|
209
|
+
serverMinVersion: number,
|
|
210
|
+
serverMaxVersion: number
|
|
211
|
+
): number | null {
|
|
212
|
+
const minVersion = Math.max(clientMinVersion, serverMinVersion)
|
|
213
|
+
const maxVersion = Math.min(clientMaxVersion, serverMaxVersion)
|
|
214
|
+
if (minVersion > maxVersion) {
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return maxVersion
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function getProcessVersion(): string {
|
|
222
|
+
return Bun.version
|
|
223
|
+
}
|
|
224
|
+
|
|
181
225
|
export function parseClientRequest(value: unknown): ClientRequest {
|
|
182
226
|
assert(isObjectRecord(value), 'IPC message must be an object')
|
|
183
227
|
assert(isString(value.id), 'IPC request id must be a string')
|
|
@@ -185,10 +229,14 @@ export function parseClientRequest(value: unknown): ClientRequest {
|
|
|
185
229
|
assert(isObjectRecord(value.payload), 'IPC request payload must be an object')
|
|
186
230
|
|
|
187
231
|
switch (value.type) {
|
|
232
|
+
case 'hello':
|
|
233
|
+
assert(isFiniteNumber(value.payload.minVersion), 'hello.minVersion must be a number')
|
|
234
|
+
assert(isFiniteNumber(value.payload.maxVersion), 'hello.maxVersion must be a number')
|
|
235
|
+
return value as ClientRequest
|
|
188
236
|
case 'attach':
|
|
189
237
|
assert(
|
|
190
|
-
value.payload.protocolVersion
|
|
191
|
-
|
|
238
|
+
isFiniteNumber(value.payload.protocolVersion),
|
|
239
|
+
'attach.protocolVersion must be a number'
|
|
192
240
|
)
|
|
193
241
|
assert(isString(value.payload.sessionId), 'attach.sessionId must be a string')
|
|
194
242
|
assert(isFiniteNumber(value.payload.cols), 'attach.cols must be a number')
|
|
@@ -258,6 +306,10 @@ export function parseServerMessage(value: unknown): ServerResponse | ServerEvent
|
|
|
258
306
|
assert(isObjectRecord(value.payload), 'IPC response payload must be an object')
|
|
259
307
|
|
|
260
308
|
switch (value.type) {
|
|
309
|
+
case 'helloResult':
|
|
310
|
+
assert(isString(value.id), 'helloResult.id must be a string')
|
|
311
|
+
assert(isProtocolHelloResult(value.payload), 'helloResult.payload is invalid')
|
|
312
|
+
return value as ServerResponse
|
|
261
313
|
case 'ok':
|
|
262
314
|
assert(isString(value.id), 'ok.id must be a string')
|
|
263
315
|
return value as ServerResponse
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { getIpcDaemonSocketPath, getTerminalManagerSocketPath } from '../daemon/runtime-paths'
|
|
5
5
|
import { logDebug } from '../debug/input-log'
|
|
6
6
|
|
|
7
7
|
const ENTRY_POINT = resolve(import.meta.dir, '..', 'index.tsx')
|
|
8
8
|
|
|
9
|
-
export async function
|
|
9
|
+
export async function findSocketProcessPid(socketPath: string): Promise<number | null> {
|
|
10
10
|
try {
|
|
11
11
|
const proc = Bun.spawn(['lsof', '-t', socketPath], { stderr: 'ignore', stdout: 'pipe' })
|
|
12
12
|
const text = await new Response(proc.stdout).text()
|
|
@@ -21,7 +21,19 @@ export async function findDaemonPid(socketPath: string): Promise<number | null>
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function
|
|
24
|
+
export async function findDaemonPid(socketPath: string): Promise<number | null> {
|
|
25
|
+
return findSocketProcessPid(socketPath)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function findIpcDaemonPid(): Promise<number | null> {
|
|
29
|
+
return findSocketProcessPid(getIpcDaemonSocketPath())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function findTerminalManagerPid(): Promise<number | null> {
|
|
33
|
+
return findSocketProcessPid(getTerminalManagerSocketPath())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function killProcess(pid: number): Promise<void> {
|
|
25
37
|
process.kill(pid, 'SIGTERM')
|
|
26
38
|
|
|
27
39
|
const deadline = Date.now() + 3_000
|
|
@@ -41,16 +53,12 @@ export async function killDaemon(pid: number): Promise<void> {
|
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
export async function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
stderr: 'ignore',
|
|
48
|
-
stdin: 'ignore',
|
|
49
|
-
stdout: 'ignore',
|
|
50
|
-
}).unref()
|
|
56
|
+
export async function killDaemon(pid: number): Promise<void> {
|
|
57
|
+
await killProcess(pid)
|
|
58
|
+
}
|
|
51
59
|
|
|
60
|
+
async function waitForSocket(socketPath: string): Promise<boolean> {
|
|
52
61
|
const deadline = Date.now() + 2_000
|
|
53
|
-
const socketPath = getDaemonSocketPath()
|
|
54
62
|
while (Date.now() < deadline) {
|
|
55
63
|
if (existsSync(socketPath)) {
|
|
56
64
|
return true
|
|
@@ -60,3 +68,26 @@ export async function spawnDetachedDaemon(): Promise<boolean> {
|
|
|
60
68
|
|
|
61
69
|
return false
|
|
62
70
|
}
|
|
71
|
+
|
|
72
|
+
async function spawnDetachedProcess(command: 'daemon' | 'terminal-manager', socketPath: string) {
|
|
73
|
+
Bun.spawn([process.execPath, 'run', ENTRY_POINT, command], {
|
|
74
|
+
detached: true,
|
|
75
|
+
stderr: 'ignore',
|
|
76
|
+
stdin: 'ignore',
|
|
77
|
+
stdout: 'ignore',
|
|
78
|
+
}).unref()
|
|
79
|
+
|
|
80
|
+
return waitForSocket(socketPath)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function spawnDetachedDaemon(): Promise<boolean> {
|
|
84
|
+
return spawnDetachedProcess('daemon', getIpcDaemonSocketPath())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function spawnDetachedIpcDaemon(): Promise<boolean> {
|
|
88
|
+
return spawnDetachedDaemon()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function spawnDetachedTerminalManager(): Promise<boolean> {
|
|
92
|
+
return spawnDetachedProcess('terminal-manager', getTerminalManagerSocketPath())
|
|
93
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PROFILE = 'default'
|
|
4
|
+
|
|
5
|
+
function sanitizeProfile(profile: string): string {
|
|
6
|
+
const trimmed = profile.trim().toLowerCase()
|
|
7
|
+
const normalized = trimmed.replace(/[^a-z0-9._-]+/g, '-')
|
|
8
|
+
const collapsed = normalized.replace(/-+/g, '-').replace(/^-|-$/g, '')
|
|
9
|
+
return collapsed || DEFAULT_PROFILE
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getProfileName(): string {
|
|
13
|
+
const configured = process.env.AIMUX_PROFILE ?? process.env.AIMUX_RUNTIME_PROFILE
|
|
14
|
+
if (!configured) {
|
|
15
|
+
return DEFAULT_PROFILE
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return sanitizeProfile(configured)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getConfigProfilesRootDir(): string {
|
|
22
|
+
return join(process.env.HOME ?? '~', '.config', 'aimux')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getProfileConfigDir(): string {
|
|
26
|
+
return join(getConfigProfilesRootDir(), getProfileName())
|
|
27
|
+
}
|
package/src/pty/pty-manager.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { EventEmitter } from 'node:events'
|
|
|
4
4
|
|
|
5
5
|
import type { TerminalModeState, TerminalSnapshot } from '../state/types'
|
|
6
6
|
|
|
7
|
+
import { logDebug } from '../debug/input-log'
|
|
7
8
|
import { areTerminalSnapshotsEqual, snapshotTerminal } from './terminal-snapshot'
|
|
8
9
|
|
|
9
10
|
type PtyManagerEvents = {
|
|
@@ -99,6 +100,12 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
99
100
|
|
|
100
101
|
session.lastSnapshot = nextSnapshot
|
|
101
102
|
session.lastTerminalModes = nextTerminalModes
|
|
103
|
+
logDebug('ptyManager.render', {
|
|
104
|
+
isAlternateBuffer: nextTerminalModes.isAlternateBuffer,
|
|
105
|
+
lines: nextSnapshot.lines.length,
|
|
106
|
+
tabId: session.tabId,
|
|
107
|
+
viewportY: nextSnapshot.viewportY,
|
|
108
|
+
})
|
|
102
109
|
this.emit('render', session.tabId, nextSnapshot, nextTerminalModes)
|
|
103
110
|
}
|
|
104
111
|
|
|
@@ -111,6 +118,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
111
118
|
this.sessions.delete(session.tabId)
|
|
112
119
|
this.emitRenderIfChanged(session)
|
|
113
120
|
session.emulator.dispose()
|
|
121
|
+
logDebug('ptyManager.finalize', { exitCode, tabId: session.tabId })
|
|
114
122
|
this.emit('exit', session.tabId, exitCode)
|
|
115
123
|
}
|
|
116
124
|
|
|
@@ -123,6 +131,14 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
123
131
|
cwd?: string
|
|
124
132
|
}): void {
|
|
125
133
|
this.disposeSession(options.tabId)
|
|
134
|
+
logDebug('ptyManager.create.start', {
|
|
135
|
+
args: options.args ?? [],
|
|
136
|
+
cols: options.cols,
|
|
137
|
+
command: options.command,
|
|
138
|
+
cwd: options.cwd ?? process.cwd(),
|
|
139
|
+
rows: options.rows,
|
|
140
|
+
tabId: options.tabId,
|
|
141
|
+
})
|
|
126
142
|
|
|
127
143
|
try {
|
|
128
144
|
const emulator = new XTerm({
|
|
@@ -157,6 +173,11 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
pty.onData((data) => {
|
|
176
|
+
logDebug('ptyManager.data', {
|
|
177
|
+
byteLength: Buffer.byteLength(data, 'utf8'),
|
|
178
|
+
pendingWrites: session.pendingWrites,
|
|
179
|
+
tabId: options.tabId,
|
|
180
|
+
})
|
|
160
181
|
const trackedModes = trackPrivateModes(
|
|
161
182
|
session.alternateScrollMode,
|
|
162
183
|
session.cursorVisible,
|
|
@@ -178,6 +199,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
178
199
|
})
|
|
179
200
|
|
|
180
201
|
pty.onExit(({ exitCode }) => {
|
|
202
|
+
logDebug('ptyManager.exit', { exitCode, tabId: options.tabId })
|
|
181
203
|
const current = this.sessions.get(options.tabId)
|
|
182
204
|
if (!current || current.pty !== pty) {
|
|
183
205
|
return
|
|
@@ -192,9 +214,11 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
192
214
|
})
|
|
193
215
|
|
|
194
216
|
this.sessions.set(options.tabId, session)
|
|
217
|
+
logDebug('ptyManager.create.success', { tabId: options.tabId })
|
|
195
218
|
this.emitRenderIfChanged(session)
|
|
196
219
|
} catch (error) {
|
|
197
220
|
const message = error instanceof Error ? error.message : String(error)
|
|
221
|
+
logDebug('ptyManager.create.error', { error: message, tabId: options.tabId })
|
|
198
222
|
this.emit('error', options.tabId, `Failed to start session: ${message}`)
|
|
199
223
|
}
|
|
200
224
|
}
|
package/src/restart-daemon.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import { getDaemonSocketPath, removeDaemonSocketIfExists } from './daemon/runtime-paths'
|
|
2
|
-
import {
|
|
2
|
+
import { findIpcDaemonPid, killProcess, spawnDetachedIpcDaemon } from './platform/daemon-control'
|
|
3
3
|
|
|
4
4
|
export async function runRestartDaemon(): Promise<number> {
|
|
5
5
|
const socketPath = getDaemonSocketPath()
|
|
6
|
-
const pid = await
|
|
6
|
+
const pid = await findIpcDaemonPid()
|
|
7
7
|
|
|
8
8
|
if (pid !== null) {
|
|
9
|
-
process.stdout.write(`Stopping daemon (pid ${pid})...\n`)
|
|
10
|
-
await
|
|
11
|
-
process.stdout.write('
|
|
9
|
+
process.stdout.write(`Stopping IPC daemon (pid ${pid})...\n`)
|
|
10
|
+
await killProcess(pid)
|
|
11
|
+
process.stdout.write('IPC daemon stopped.\n')
|
|
12
12
|
} else {
|
|
13
|
-
process.stdout.write('No running daemon found.\n')
|
|
13
|
+
process.stdout.write('No running IPC daemon found.\n')
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
removeDaemonSocketIfExists()
|
|
17
17
|
|
|
18
|
-
process.stdout.write('Starting daemon...\n')
|
|
19
|
-
const ok = await
|
|
18
|
+
process.stdout.write('Starting IPC daemon...\n')
|
|
19
|
+
const ok = await spawnDetachedIpcDaemon()
|
|
20
20
|
|
|
21
21
|
if (ok) {
|
|
22
|
-
process.stdout.write(
|
|
22
|
+
process.stdout.write(`IPC daemon started on ${socketPath}.\n`)
|
|
23
23
|
return 0
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
process.stderr.write('Failed to start daemon.\n')
|
|
26
|
+
process.stderr.write('Failed to start IPC daemon.\n')
|
|
27
27
|
return 1
|
|
28
28
|
}
|