@brimveyn/aimux 1.2.5 → 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.
@@ -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 }
@@ -7,7 +7,20 @@ import type {
7
7
 
8
8
  import { isWorkspaceSnapshotV1 } from '../state/validation'
9
9
 
10
- export const IPC_PROTOCOL_VERSION = 1
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 === IPC_PROTOCOL_VERSION,
191
- `attach.protocolVersion must be ${IPC_PROTOCOL_VERSION}`
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 { getDaemonSocketPath } from '../daemon/runtime-paths'
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 findDaemonPid(socketPath: string): Promise<number | null> {
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 killDaemon(pid: number): Promise<void> {
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 spawnDetachedDaemon(): Promise<boolean> {
45
- Bun.spawn([process.execPath, 'run', ENTRY_POINT, 'daemon'], {
46
- detached: true,
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
+ }
@@ -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
  }
@@ -1,28 +1,28 @@
1
1
  import { getDaemonSocketPath, removeDaemonSocketIfExists } from './daemon/runtime-paths'
2
- import { findDaemonPid, killDaemon, spawnDetachedDaemon } from './platform/daemon-control'
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 findDaemonPid(socketPath)
6
+ const pid = await findIpcDaemonPid()
7
7
 
8
8
  if (pid !== null) {
9
- process.stdout.write(`Stopping daemon (pid ${pid})...\n`)
10
- await killDaemon(pid)
11
- process.stdout.write('Daemon stopped.\n')
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 spawnDetachedDaemon()
18
+ process.stdout.write('Starting IPC daemon...\n')
19
+ const ok = await spawnDetachedIpcDaemon()
20
20
 
21
21
  if (ok) {
22
- process.stdout.write('Daemon started.\n')
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
  }