loadtoagent 1.1.0 → 1.3.2

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.
Files changed (86) hide show
  1. package/README.ko.md +14 -5
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +7 -0
  4. package/docs/PROVIDER-CONTRACTS.md +25 -1
  5. package/docs/UI-AUDIT-100.md +118 -0
  6. package/docs/UI-AUDIT-101-200.md +120 -0
  7. package/docs/UI-AUDIT-201-300.md +120 -0
  8. package/main.js +157 -38
  9. package/package.json +13 -4
  10. package/preload.js +13 -2
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -19
  13. package/renderer/app-dashboard.js +196 -55
  14. package/renderer/app-drawer-content.js +48 -44
  15. package/renderer/app-drawer-data.js +26 -13
  16. package/renderer/app-drawer.js +72 -54
  17. package/renderer/app-events-dialogs.js +146 -37
  18. package/renderer/app-events-filters.js +172 -13
  19. package/renderer/app-events-navigation.js +77 -30
  20. package/renderer/app-events-sessions.js +156 -9
  21. package/renderer/app-events.js +2 -1
  22. package/renderer/app-graph-model.js +20 -38
  23. package/renderer/app-graph-orchestration.js +15 -13
  24. package/renderer/app-graph-view.js +228 -113
  25. package/renderer/app-management.js +257 -0
  26. package/renderer/app-provider-visibility.js +124 -0
  27. package/renderer/app-quality.js +568 -0
  28. package/renderer/app-run-modal.js +104 -34
  29. package/renderer/app-runtime-overview.js +312 -0
  30. package/renderer/app-session-render.js +94 -37
  31. package/renderer/app-tmux-render.js +52 -44
  32. package/renderer/app.js +153 -75
  33. package/renderer/i18n-messages.js +867 -37
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +154 -67
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +16 -10
  38. package/renderer/styles-cards.css +44 -18
  39. package/renderer/styles-collaboration.css +23 -23
  40. package/renderer/styles-components.css +122 -45
  41. package/renderer/styles-management.css +364 -0
  42. package/renderer/styles-onboarding.css +8 -8
  43. package/renderer/styles-overlays.css +13 -7
  44. package/renderer/styles-product.css +48 -40
  45. package/renderer/styles-quality.css +312 -0
  46. package/renderer/styles-readability.css +1040 -0
  47. package/renderer/styles-responsive-product.css +84 -12
  48. package/renderer/styles-responsive-runtime.css +25 -17
  49. package/renderer/styles-responsive-shell.css +44 -48
  50. package/renderer/styles-responsive-workflows.css +39 -5
  51. package/renderer/styles-run-composer.css +28 -23
  52. package/renderer/styles-runtime-overview.css +285 -0
  53. package/renderer/styles-settings.css +176 -16
  54. package/renderer/styles-terminal.css +374 -37
  55. package/renderer/styles-tmux.css +49 -49
  56. package/renderer/styles-workflow-map.css +362 -15
  57. package/renderer/styles-workflows.css +116 -49
  58. package/renderer/styles.css +34 -19
  59. package/renderer/terminal-agent.js +17 -13
  60. package/renderer/terminal-events.js +253 -37
  61. package/renderer/terminal-workbench.js +208 -89
  62. package/renderer/terminal.js +350 -35
  63. package/src/agentMonitor/claudeParser.js +96 -7
  64. package/src/agentMonitor/codexParser.js +59 -4
  65. package/src/agentMonitor/executionActivity.js +282 -0
  66. package/src/agentMonitor/genericParser.js +56 -13
  67. package/src/agentMonitor/hierarchy.js +17 -0
  68. package/src/agentMonitor/responseIntent.js +51 -0
  69. package/src/agentMonitor.js +21 -3
  70. package/src/agentRunner.js +66 -1
  71. package/src/attentionNotifier.js +14 -10
  72. package/src/automationMonitor.js +258 -0
  73. package/src/contracts.js +10 -1
  74. package/src/ipc/registerAgentIpc.js +9 -3
  75. package/src/ipc/registerAppIpc.js +4 -1
  76. package/src/ipc/registerTerminalIpc.js +12 -14
  77. package/src/macUpdateHelper.js +210 -0
  78. package/src/monitorWorker.js +65 -6
  79. package/src/platformPath.js +80 -0
  80. package/src/processMonitor.js +80 -22
  81. package/src/providerVisibilityStore.js +45 -0
  82. package/src/sessionIntelligence.js +261 -0
  83. package/src/terminalHost.js +453 -0
  84. package/src/terminalHostDaemon.js +61 -0
  85. package/src/terminalManager.js +218 -6
  86. package/src/updateInstaller.js +175 -0
@@ -0,0 +1,453 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const net = require('net');
7
+ const crypto = require('crypto');
8
+ const { EventEmitter } = require('events');
9
+ const { spawn } = require('child_process');
10
+ const { endpointFor, safeWriteJson } = require('./bridgeServer');
11
+ const { runBestEffort } = require('./diagnostics');
12
+
13
+ const TERMINAL_HOST_PROTOCOL = 1;
14
+ const MAX_FRAME_CHARS = 4 * 1024 * 1024;
15
+ const AUTH_TIMEOUT_MS = 5_000;
16
+ const REQUEST_TIMEOUT_MS = 15_000;
17
+ const HOST_OPERATIONS = new Set(['list', 'get', 'create', 'write', 'command', 'resize', 'signal', 'restart', 'close']);
18
+
19
+ function sendFrame(socket, payload) {
20
+ if (!socket || socket.destroyed) return;
21
+ socket.write(`${JSON.stringify(payload)}\n`, 'utf8');
22
+ }
23
+
24
+ function readHostDiscovery(file, fileSystem = fs) {
25
+ const parsed = JSON.parse(fileSystem.readFileSync(file, 'utf8'));
26
+ if (parsed?.protocol !== TERMINAL_HOST_PROTOCOL || !parsed.endpoint || !parsed.token) {
27
+ throw new Error('터미널 호스트 연결 정보가 올바르지 않습니다.');
28
+ }
29
+ return parsed;
30
+ }
31
+
32
+ function activeSessions(manager) {
33
+ return manager.list().filter(session => session.status === 'running' || session.status === 'starting');
34
+ }
35
+
36
+ class TerminalHostServer {
37
+ constructor(options = {}) {
38
+ this.manager = options.manager;
39
+ this.platform = options.platform || process.platform;
40
+ this.discoveryFile = path.resolve(options.discoveryFile || path.join(os.tmpdir(), 'loadtoagent-terminal-host.json'));
41
+ this.endpoint = options.endpoint || endpointFor(this.platform, `${path.dirname(this.discoveryFile)}:terminal-host`);
42
+ this.token = options.token || crypto.randomBytes(32).toString('hex');
43
+ this.server = null;
44
+ this.clients = new Set();
45
+ this.shutdownTimer = null;
46
+ this.idleShutdownMs = Number.isFinite(Number(options.idleShutdownMs))
47
+ ? Math.max(0, Number(options.idleShutdownMs))
48
+ : 1_500;
49
+ this.onShutdown = typeof options.onShutdown === 'function' ? options.onShutdown : () => {};
50
+ this.onManagerData = payload => this.broadcast({ type: 'event', event: 'data', payload });
51
+ this.onManagerState = payload => {
52
+ this.broadcast({ type: 'event', event: 'state', payload });
53
+ this.scheduleShutdownIfIdle();
54
+ };
55
+ }
56
+
57
+ info() {
58
+ return {
59
+ protocol: TERMINAL_HOST_PROTOCOL,
60
+ endpoint: this.endpoint,
61
+ token: this.token,
62
+ pid: process.pid,
63
+ platform: this.platform,
64
+ updatedAt: new Date().toISOString(),
65
+ };
66
+ }
67
+
68
+ start() {
69
+ if (!this.manager) return Promise.reject(new Error('터미널 관리자가 준비되지 않았습니다.'));
70
+ if (this.server) return Promise.resolve(this.info());
71
+ if (this.platform !== 'win32' && fs.existsSync(this.endpoint)) {
72
+ runBestEffort('terminal-host-stale-endpoint', () => fs.unlinkSync(this.endpoint));
73
+ }
74
+ this.server = net.createServer(socket => this.accept(socket));
75
+ return new Promise((resolve, reject) => {
76
+ const fail = error => {
77
+ if (this.server) runBestEffort('terminal-host-start-close', () => this.server.close());
78
+ this.server = null;
79
+ reject(error);
80
+ };
81
+ this.server.once('error', fail);
82
+ this.server.listen(this.endpoint, () => {
83
+ this.server.removeListener('error', fail);
84
+ try {
85
+ safeWriteJson(this.discoveryFile, this.info());
86
+ this.manager.on('data', this.onManagerData);
87
+ this.manager.on('state', this.onManagerState);
88
+ this.scheduleShutdownIfIdle();
89
+ resolve(this.info());
90
+ } catch (error) {
91
+ fail(error);
92
+ }
93
+ });
94
+ });
95
+ }
96
+
97
+ accept(socket) {
98
+ this.cancelIdleShutdown();
99
+ socket.setNoDelay(true);
100
+ const client = {
101
+ socket,
102
+ buffer: '',
103
+ authenticated: false,
104
+ queue: Promise.resolve(),
105
+ authTimer: setTimeout(() => socket.destroy(new Error('터미널 호스트 인증 시간이 초과되었습니다.')), AUTH_TIMEOUT_MS),
106
+ };
107
+ this.clients.add(client);
108
+ socket.on('data', chunk => this.consume(client, chunk));
109
+ socket.on('error', () => this.detach(client));
110
+ socket.on('close', () => this.detach(client));
111
+ }
112
+
113
+ consume(client, chunk) {
114
+ client.buffer += chunk.toString('utf8');
115
+ if (client.buffer.length > MAX_FRAME_CHARS) {
116
+ client.socket.destroy(new Error('터미널 호스트 요청이 너무 큽니다.'));
117
+ return;
118
+ }
119
+ let newline;
120
+ while ((newline = client.buffer.indexOf('\n')) >= 0) {
121
+ const line = client.buffer.slice(0, newline).trim();
122
+ client.buffer = client.buffer.slice(newline + 1);
123
+ if (!line) continue;
124
+ let message;
125
+ try {
126
+ message = JSON.parse(line);
127
+ } catch (_invalidFrame) {
128
+ client.socket.destroy(new Error('터미널 호스트 요청 형식이 올바르지 않습니다.'));
129
+ return;
130
+ }
131
+ client.queue = client.queue
132
+ .then(() => this.handle(client, message || {}))
133
+ .catch(error => sendFrame(client.socket, {
134
+ type: 'response',
135
+ requestId: String(message?.requestId || ''),
136
+ ok: false,
137
+ error: String(error?.message || error),
138
+ }));
139
+ }
140
+ }
141
+
142
+ async handle(client, message) {
143
+ if (!client.authenticated) {
144
+ if (message.type !== 'authenticate' || message.token !== this.token) {
145
+ throw new Error('터미널 호스트 인증에 실패했습니다.');
146
+ }
147
+ client.authenticated = true;
148
+ clearTimeout(client.authTimer);
149
+ client.authTimer = null;
150
+ this.cancelIdleShutdown();
151
+ sendFrame(client.socket, { type: 'ready', sessions: this.manager.list() });
152
+ return;
153
+ }
154
+ if (message.type === 'control' && message.operation === 'shutdown-if-idle') {
155
+ this.scheduleShutdownIfIdle();
156
+ return;
157
+ }
158
+ if (message.type !== 'request' || !HOST_OPERATIONS.has(message.operation)) {
159
+ throw new Error('지원하지 않는 터미널 호스트 작업입니다.');
160
+ }
161
+ const operation = message.operation;
162
+ const args = Array.isArray(message.args) ? message.args : [];
163
+ const result = await Promise.resolve(this.manager[operation](...args));
164
+ sendFrame(client.socket, { type: 'response', requestId: String(message.requestId || ''), ok: true, result });
165
+ }
166
+
167
+ broadcast(payload) {
168
+ for (const client of this.clients) {
169
+ if (client.authenticated) sendFrame(client.socket, payload);
170
+ }
171
+ }
172
+
173
+ detach(client) {
174
+ if (client.authTimer) clearTimeout(client.authTimer);
175
+ this.clients.delete(client);
176
+ this.scheduleShutdownIfIdle();
177
+ }
178
+
179
+ cancelIdleShutdown() {
180
+ if (!this.shutdownTimer) return;
181
+ clearTimeout(this.shutdownTimer);
182
+ this.shutdownTimer = null;
183
+ }
184
+
185
+ scheduleShutdownIfIdle() {
186
+ const connectedClients = [...this.clients].filter(entry => entry.authenticated && !entry.socket.destroyed);
187
+ if (activeSessions(this.manager).length > 0 || connectedClients.length > 0) {
188
+ this.cancelIdleShutdown();
189
+ return;
190
+ }
191
+ if (this.shutdownTimer) return;
192
+ this.shutdownTimer = setTimeout(() => {
193
+ this.shutdownTimer = null;
194
+ const activeClients = [...this.clients].filter(entry => entry.authenticated && !entry.socket.destroyed);
195
+ if (activeSessions(this.manager).length === 0 && activeClients.length === 0) this.onShutdown();
196
+ }, this.idleShutdownMs);
197
+ if (typeof this.shutdownTimer.unref === 'function') this.shutdownTimer.unref();
198
+ }
199
+
200
+ dispose() {
201
+ if (this.shutdownTimer) clearTimeout(this.shutdownTimer);
202
+ this.shutdownTimer = null;
203
+ this.manager.removeListener('data', this.onManagerData);
204
+ this.manager.removeListener('state', this.onManagerState);
205
+ for (const client of this.clients) runBestEffort('terminal-host-client-close', () => client.socket.destroy());
206
+ this.clients.clear();
207
+ if (this.server) runBestEffort('terminal-host-server-close', () => this.server.close());
208
+ this.server = null;
209
+ try {
210
+ const current = readHostDiscovery(this.discoveryFile);
211
+ if (current.pid === process.pid && current.token === this.token) fs.unlinkSync(this.discoveryFile);
212
+ } catch (_missingOrReplacedDiscovery) {}
213
+ if (this.platform !== 'win32' && fs.existsSync(this.endpoint)) {
214
+ runBestEffort('terminal-host-endpoint-cleanup', () => fs.unlinkSync(this.endpoint));
215
+ }
216
+ }
217
+ }
218
+
219
+ function launchTerminalHost(options = {}) {
220
+ const executable = options.executable || process.execPath;
221
+ const script = options.script;
222
+ if (!script) throw new Error('터미널 호스트 스크립트 경로가 없습니다.');
223
+ const config = Buffer.from(JSON.stringify({
224
+ storeFile: options.storeFile,
225
+ discoveryFile: options.discoveryFile,
226
+ bridgeHome: options.bridgeHome,
227
+ }), 'utf8').toString('base64');
228
+ fs.mkdirSync(path.dirname(options.discoveryFile), { recursive: true });
229
+ const child = (options.spawnProcess || spawn)(executable, [script, '--config', config], {
230
+ cwd: path.dirname(options.discoveryFile),
231
+ detached: true,
232
+ windowsHide: true,
233
+ stdio: 'ignore',
234
+ env: { ...process.env, ...options.env, ELECTRON_RUN_AS_NODE: '1' },
235
+ });
236
+ child.unref();
237
+ return child.pid;
238
+ }
239
+
240
+ function resolveTerminalHostExecutable(options = {}) {
241
+ const platform = options.platform || process.platform;
242
+ const executable = path.resolve(options.executable || process.execPath);
243
+ if (platform !== 'darwin' || !options.isPackaged) return executable;
244
+ const productName = path.basename(executable);
245
+ const helper = path.resolve(
246
+ path.dirname(executable),
247
+ '..',
248
+ 'Frameworks',
249
+ `${productName} Helper.app`,
250
+ 'Contents',
251
+ 'MacOS',
252
+ `${productName} Helper`,
253
+ );
254
+ const fileSystem = options.fileSystem || fs;
255
+ return fileSystem.existsSync(helper) ? helper : executable;
256
+ }
257
+
258
+ class TerminalHostClient extends EventEmitter {
259
+ constructor(options = {}) {
260
+ super();
261
+ this.discoveryFile = path.resolve(options.discoveryFile);
262
+ this.spawnHost = typeof options.spawnHost === 'function' ? options.spawnHost : null;
263
+ this.connectTimeoutMs = Number(options.connectTimeoutMs || 12_000);
264
+ this.socket = null;
265
+ this.buffer = '';
266
+ this.connected = false;
267
+ this.disposed = false;
268
+ this.sessions = [];
269
+ this.sequence = 0;
270
+ this.pending = new Map();
271
+ this.handshake = null;
272
+ this.connectPromise = null;
273
+ this.connectGeneration = 0;
274
+ }
275
+
276
+ connect() {
277
+ if (this.connected && this.socket && !this.socket.destroyed) return Promise.resolve(this);
278
+ if (this.connectPromise) return this.connectPromise;
279
+ this.disposed = false;
280
+ const generation = ++this.connectGeneration;
281
+ const connecting = this.connectLoop(generation);
282
+ const tracked = connecting.finally(() => {
283
+ if (this.connectPromise === tracked) this.connectPromise = null;
284
+ });
285
+ this.connectPromise = tracked;
286
+ return this.connectPromise;
287
+ }
288
+
289
+ async connectLoop(generation) {
290
+ const deadline = Date.now() + this.connectTimeoutMs;
291
+ let launched = false;
292
+ let lastError = null;
293
+ while (!this.disposed && generation === this.connectGeneration && Date.now() < deadline) {
294
+ try {
295
+ await this.connectExisting();
296
+ if (this.disposed || generation !== this.connectGeneration) throw new Error('터미널 호스트 재연결이 취소되었습니다.');
297
+ return this;
298
+ } catch (error) {
299
+ lastError = error;
300
+ this.resetSocket();
301
+ }
302
+ if (!launched) {
303
+ if (!this.spawnHost) throw lastError;
304
+ await Promise.resolve(this.spawnHost());
305
+ launched = true;
306
+ }
307
+ await new Promise(resolve => setTimeout(resolve, 120));
308
+ }
309
+ if (this.disposed || generation !== this.connectGeneration) throw new Error('터미널 호스트 재연결이 취소되었습니다.');
310
+ throw new Error(`터미널 호스트에 연결하지 못했습니다: ${lastError?.message || '시간 초과'}`);
311
+ }
312
+
313
+ connectExisting() {
314
+ const discovery = readHostDiscovery(this.discoveryFile);
315
+ return new Promise((resolve, reject) => {
316
+ const socket = net.createConnection(discovery.endpoint);
317
+ const timer = setTimeout(() => {
318
+ socket.destroy();
319
+ reject(new Error('터미널 호스트 연결 시간이 초과되었습니다.'));
320
+ }, 1_500);
321
+ this.socket = socket;
322
+ this.buffer = '';
323
+ this.handshake = {
324
+ resolve: () => { clearTimeout(timer); this.handshake = null; resolve(); },
325
+ reject: error => { clearTimeout(timer); this.handshake = null; reject(error); },
326
+ };
327
+ socket.setNoDelay(true);
328
+ socket.on('connect', () => sendFrame(socket, { type: 'authenticate', token: discovery.token }));
329
+ socket.on('data', chunk => this.consume(chunk, socket));
330
+ socket.on('error', error => this.handleSocketError(socket, error));
331
+ socket.on('close', () => this.handleDisconnect(socket));
332
+ });
333
+ }
334
+
335
+ handleSocketError(socket, error) {
336
+ if (socket && socket !== this.socket) return;
337
+ if (this.handshake) this.handshake.reject(error);
338
+ }
339
+
340
+ consume(chunk, socket = this.socket) {
341
+ if (socket && socket !== this.socket) return;
342
+ this.buffer += chunk.toString('utf8');
343
+ if (this.buffer.length > MAX_FRAME_CHARS) {
344
+ this.socket?.destroy(new Error('터미널 호스트 응답이 너무 큽니다.'));
345
+ return;
346
+ }
347
+ let newline;
348
+ while ((newline = this.buffer.indexOf('\n')) >= 0) {
349
+ const line = this.buffer.slice(0, newline).trim();
350
+ this.buffer = this.buffer.slice(newline + 1);
351
+ if (!line) continue;
352
+ let message;
353
+ try { message = JSON.parse(line); } catch { continue; }
354
+ if (message.type === 'ready') {
355
+ this.sessions = Array.isArray(message.sessions) ? message.sessions : [];
356
+ this.connected = true;
357
+ if (this.handshake) this.handshake.resolve();
358
+ } else if (message.type === 'response') {
359
+ const pending = this.pending.get(String(message.requestId || ''));
360
+ if (!pending) continue;
361
+ this.pending.delete(String(message.requestId || ''));
362
+ clearTimeout(pending.timer);
363
+ if (message.ok) pending.resolve(message.result);
364
+ else pending.reject(new Error(String(message.error || '터미널 호스트 작업 실패')));
365
+ } else if (message.type === 'event' && message.event === 'data') {
366
+ this.emit('data', message.payload);
367
+ } else if (message.type === 'event' && message.event === 'state') {
368
+ if (Array.isArray(message.payload?.sessions)) this.sessions = message.payload.sessions;
369
+ this.emit('state', message.payload);
370
+ }
371
+ }
372
+ }
373
+
374
+ handleDisconnect(socket = this.socket) {
375
+ if (socket && socket !== this.socket) return;
376
+ const wasConnected = this.connected;
377
+ this.connected = false;
378
+ if (this.handshake) this.handshake.reject(new Error('터미널 호스트 연결이 닫혔습니다.'));
379
+ for (const pending of this.pending.values()) {
380
+ clearTimeout(pending.timer);
381
+ pending.reject(new Error('터미널 호스트 연결이 닫혔습니다.'));
382
+ }
383
+ this.pending.clear();
384
+ this.socket = null;
385
+ if (wasConnected && !this.disposed) {
386
+ this.emit('disconnect');
387
+ this.connect()
388
+ .then(() => this.emit('reconnect', { sessions: this.list() }))
389
+ .catch(error => {
390
+ if (!this.disposed) this.emit('reconnect-error', error);
391
+ });
392
+ }
393
+ }
394
+
395
+ resetSocket() {
396
+ const socket = this.socket;
397
+ this.socket = null;
398
+ this.connected = false;
399
+ this.buffer = '';
400
+ if (socket) socket.destroy();
401
+ }
402
+
403
+ async request(operation, ...args) {
404
+ if (!this.connected || !this.socket || this.socket.destroyed) {
405
+ await this.connect();
406
+ }
407
+ if (!this.connected || !this.socket || this.socket.destroyed) {
408
+ throw new Error('터미널 호스트에 연결되어 있지 않습니다.');
409
+ }
410
+ const requestId = String(++this.sequence);
411
+ return new Promise((resolve, reject) => {
412
+ const timer = setTimeout(() => {
413
+ this.pending.delete(requestId);
414
+ reject(new Error(`터미널 호스트 작업 시간이 초과되었습니다: ${operation}`));
415
+ }, REQUEST_TIMEOUT_MS);
416
+ this.pending.set(requestId, { resolve, reject, timer });
417
+ sendFrame(this.socket, { type: 'request', requestId, operation, args });
418
+ });
419
+ }
420
+
421
+ list() { return this.sessions.map(session => ({ ...session })); }
422
+ get(id, includeReplay = true) { return this.request('get', id, includeReplay); }
423
+ create(options) { return this.request('create', options); }
424
+ write(id, data) { return this.request('write', id, data); }
425
+ command(id, command) { return this.request('command', id, command); }
426
+ resize(id, cols, rows) { return this.request('resize', id, cols, rows); }
427
+ signal(id, signal) { return this.request('signal', id, signal); }
428
+ restart(id) { return this.request('restart', id); }
429
+ close(id) { return this.request('close', id); }
430
+
431
+ dispose({ shutdownIfIdle = false } = {}) {
432
+ this.disposed = true;
433
+ this.connectGeneration += 1;
434
+ if (this.socket && !this.socket.destroyed) {
435
+ if (shutdownIfIdle) sendFrame(this.socket, { type: 'control', operation: 'shutdown-if-idle' });
436
+ this.socket.end();
437
+ }
438
+ for (const pending of this.pending.values()) {
439
+ clearTimeout(pending.timer);
440
+ pending.reject(new Error('터미널 호스트 클라이언트가 종료되었습니다.'));
441
+ }
442
+ this.pending.clear();
443
+ }
444
+ }
445
+
446
+ module.exports = {
447
+ TerminalHostServer,
448
+ TerminalHostClient,
449
+ TERMINAL_HOST_PROTOCOL,
450
+ readHostDiscovery,
451
+ launchTerminalHost,
452
+ resolveTerminalHostExecutable,
453
+ };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { TerminalManager } = require('./terminalManager');
6
+ const { BridgeServer } = require('./bridgeServer');
7
+ const { TerminalHostServer } = require('./terminalHost');
8
+
9
+ function parseConfig(argv = process.argv.slice(2)) {
10
+ const index = argv.indexOf('--config');
11
+ if (index < 0 || !argv[index + 1]) throw new Error('터미널 호스트 설정이 없습니다.');
12
+ const parsed = JSON.parse(Buffer.from(argv[index + 1], 'base64').toString('utf8'));
13
+ for (const key of ['storeFile', 'discoveryFile', 'bridgeHome']) {
14
+ if (!parsed[key] || typeof parsed[key] !== 'string') throw new Error(`터미널 호스트 설정이 올바르지 않습니다: ${key}`);
15
+ }
16
+ return {
17
+ storeFile: path.resolve(parsed.storeFile),
18
+ discoveryFile: path.resolve(parsed.discoveryFile),
19
+ bridgeHome: path.resolve(parsed.bridgeHome),
20
+ };
21
+ }
22
+
23
+ async function run(config = parseConfig()) {
24
+ process.title = 'LoadToAgent Terminal Host';
25
+ const manager = new TerminalManager({ storeFile: config.storeFile });
26
+ manager.recoverPersistedSessions();
27
+ let stopping = false;
28
+ let host = null;
29
+ let bridge = null;
30
+ const stop = () => {
31
+ if (stopping) return;
32
+ stopping = true;
33
+ if (bridge) bridge.dispose();
34
+ if (host) host.dispose();
35
+ manager.dispose({ preserveSessions: true });
36
+ setImmediate(() => process.exit(0));
37
+ };
38
+ host = new TerminalHostServer({ manager, discoveryFile: config.discoveryFile, onShutdown: stop });
39
+ bridge = new BridgeServer({ terminalManager: manager, home: config.bridgeHome, platform: process.platform });
40
+ await host.start();
41
+ try { await bridge.start(); } catch (error) {
42
+ const logFile = path.join(path.dirname(config.discoveryFile), 'terminal-host.log');
43
+ fs.appendFileSync(logFile, `${new Date().toISOString()} bridge: ${error.stack || error.message}\n`, 'utf8');
44
+ }
45
+ process.on('SIGINT', stop);
46
+ process.on('SIGTERM', stop);
47
+ return { manager, host, bridge, stop };
48
+ }
49
+
50
+ if (require.main === module) {
51
+ run().catch(error => {
52
+ try {
53
+ const config = parseConfig();
54
+ fs.mkdirSync(path.dirname(config.discoveryFile), { recursive: true });
55
+ fs.appendFileSync(path.join(path.dirname(config.discoveryFile), 'terminal-host.log'), `${new Date().toISOString()} fatal: ${error.stack || error.message}\n`, 'utf8');
56
+ } catch {}
57
+ process.exitCode = 1;
58
+ });
59
+ }
60
+
61
+ module.exports = { parseConfig, run };