@goondan/cli 0.0.3-alpha6 → 0.0.3-alpha8

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,751 @@
1
+ import { createServer } from 'node:http';
2
+ import path from 'node:path';
3
+ import { readdir, stat } from 'node:fs/promises';
4
+ import { STUDIO_CSS, STUDIO_HTML, STUDIO_JS } from '../studio/assets.js';
5
+ import { exists, isObjectRecord, readTextFileIfExists } from '../utils.js';
6
+ import { resolveStateRoot } from './config.js';
7
+ function sanitizeInstanceKey(instanceKey) {
8
+ return instanceKey.replace(/[^a-zA-Z0-9_:-]/g, '-').slice(0, 128);
9
+ }
10
+ function nowIso(offset = 0) {
11
+ return new Date(Date.now() + offset).toISOString();
12
+ }
13
+ function normalizeTimestamp(input, fallbackOffset) {
14
+ if (typeof input === 'string') {
15
+ const parsed = Date.parse(input);
16
+ if (!Number.isNaN(parsed)) {
17
+ return new Date(parsed).toISOString();
18
+ }
19
+ }
20
+ return nowIso(fallbackOffset);
21
+ }
22
+ function toMillis(input) {
23
+ const parsed = Date.parse(input);
24
+ if (Number.isNaN(parsed)) {
25
+ return Number.MAX_SAFE_INTEGER;
26
+ }
27
+ return parsed;
28
+ }
29
+ function toDetailText(value) {
30
+ if (typeof value === 'string') {
31
+ return value;
32
+ }
33
+ if (Array.isArray(value)) {
34
+ const texts = value
35
+ .map((item) => {
36
+ if (typeof item === 'string') {
37
+ return item;
38
+ }
39
+ if (!isObjectRecord(item)) {
40
+ return '';
41
+ }
42
+ const textValue = item['text'];
43
+ return typeof textValue === 'string' ? textValue : '';
44
+ })
45
+ .filter((text) => text.length > 0);
46
+ return texts.join(' ');
47
+ }
48
+ if (isObjectRecord(value)) {
49
+ const textValue = value['text'];
50
+ if (typeof textValue === 'string') {
51
+ return textValue;
52
+ }
53
+ }
54
+ return '';
55
+ }
56
+ function messageFromUnknown(value) {
57
+ if (!isObjectRecord(value)) {
58
+ return undefined;
59
+ }
60
+ const createdAt = value['createdAt'];
61
+ const source = value['source'];
62
+ const data = value['data'];
63
+ if (typeof createdAt !== 'string' || !isObjectRecord(source) || !isObjectRecord(data)) {
64
+ return undefined;
65
+ }
66
+ const sourceType = source['type'];
67
+ if (typeof sourceType !== 'string') {
68
+ return undefined;
69
+ }
70
+ const role = data['role'];
71
+ const content = toDetailText(data['content']);
72
+ const extensionName = source['extensionName'];
73
+ const toolName = source['toolName'];
74
+ let finalSourceType = sourceType;
75
+ if (typeof role === 'string' && role.length > 0 && sourceType === 'assistant') {
76
+ finalSourceType = role;
77
+ }
78
+ return {
79
+ createdAt,
80
+ sourceType: finalSourceType,
81
+ toolName: typeof toolName === 'string' ? toolName : undefined,
82
+ extensionName: typeof extensionName === 'string' ? extensionName : undefined,
83
+ content,
84
+ };
85
+ }
86
+ function parseJsonLine(line) {
87
+ try {
88
+ return JSON.parse(line);
89
+ }
90
+ catch {
91
+ return undefined;
92
+ }
93
+ }
94
+ async function readJsonLines(filePath) {
95
+ const raw = await readTextFileIfExists(filePath);
96
+ if (!raw) {
97
+ return [];
98
+ }
99
+ return raw
100
+ .split('\n')
101
+ .map((line) => line.trim())
102
+ .filter((line) => line.length > 0)
103
+ .map(parseJsonLine)
104
+ .filter((line) => line !== undefined);
105
+ }
106
+ function classifyParticipantKind(sourceType) {
107
+ if (sourceType === 'user') {
108
+ return 'user';
109
+ }
110
+ if (sourceType === 'assistant') {
111
+ return 'assistant';
112
+ }
113
+ if (sourceType === 'tool') {
114
+ return 'tool';
115
+ }
116
+ if (sourceType === 'extension') {
117
+ return 'extension';
118
+ }
119
+ if (sourceType === 'system') {
120
+ return 'system';
121
+ }
122
+ return 'unknown';
123
+ }
124
+ function registerParticipant(participants, id, label, kind, at) {
125
+ const existing = participants.get(id);
126
+ if (!existing) {
127
+ participants.set(id, {
128
+ id,
129
+ label,
130
+ kind,
131
+ lastSeenAt: at,
132
+ order: participants.size,
133
+ });
134
+ return;
135
+ }
136
+ if (toMillis(at) >= toMillis(existing.lastSeenAt)) {
137
+ existing.lastSeenAt = at;
138
+ }
139
+ }
140
+ function edgeKeyFor(from, to) {
141
+ if (from <= to) {
142
+ return {
143
+ key: `${from}|${to}`,
144
+ a: from,
145
+ b: to,
146
+ forward: true,
147
+ };
148
+ }
149
+ return {
150
+ key: `${to}|${from}`,
151
+ a: to,
152
+ b: from,
153
+ forward: false,
154
+ };
155
+ }
156
+ function registerInteraction(interactions, from, to, at, kind, detail) {
157
+ if (from.length === 0 || to.length === 0 || from === to) {
158
+ return;
159
+ }
160
+ const edge = edgeKeyFor(from, to);
161
+ const existing = interactions.get(edge.key);
162
+ const history = {
163
+ at,
164
+ from,
165
+ to,
166
+ direction: edge.forward ? 'a->b' : 'b->a',
167
+ kind,
168
+ detail,
169
+ };
170
+ if (!existing) {
171
+ interactions.set(edge.key, {
172
+ key: edge.key,
173
+ a: edge.a,
174
+ b: edge.b,
175
+ total: 1,
176
+ lastSeenAt: at,
177
+ direction: edge.forward ? 'a->b' : 'b->a',
178
+ history: [history],
179
+ forwardSeen: edge.forward,
180
+ backwardSeen: !edge.forward,
181
+ });
182
+ return;
183
+ }
184
+ existing.total += 1;
185
+ if (toMillis(at) >= toMillis(existing.lastSeenAt)) {
186
+ existing.lastSeenAt = at;
187
+ }
188
+ existing.history.push(history);
189
+ if (edge.forward) {
190
+ existing.forwardSeen = true;
191
+ }
192
+ else {
193
+ existing.backwardSeen = true;
194
+ }
195
+ if (existing.forwardSeen && existing.backwardSeen) {
196
+ existing.direction = 'undirected';
197
+ }
198
+ else {
199
+ existing.direction = existing.forwardSeen ? 'a->b' : 'b->a';
200
+ }
201
+ }
202
+ function pushTimeline(timeline, entry, sequence) {
203
+ timeline.push({
204
+ entry,
205
+ sortAt: toMillis(entry.at),
206
+ sequence,
207
+ });
208
+ }
209
+ function fromMessageToRoute(message, instanceKey, defaultAgentId) {
210
+ const userId = `user:${instanceKey}`;
211
+ const systemId = 'system:runtime';
212
+ if (message.sourceType === 'user') {
213
+ return {
214
+ from: userId,
215
+ to: defaultAgentId,
216
+ kind: 'message.user',
217
+ detail: message.content,
218
+ };
219
+ }
220
+ if (message.sourceType === 'tool') {
221
+ const toolName = message.toolName ?? 'unknown';
222
+ return {
223
+ from: `tool:${toolName}`,
224
+ to: defaultAgentId,
225
+ kind: 'message.tool',
226
+ detail: message.content,
227
+ };
228
+ }
229
+ if (message.sourceType === 'extension') {
230
+ const extensionName = message.extensionName ?? 'unknown';
231
+ return {
232
+ from: `extension:${extensionName}`,
233
+ to: defaultAgentId,
234
+ kind: 'message.extension',
235
+ detail: message.content,
236
+ };
237
+ }
238
+ if (message.sourceType === 'system') {
239
+ return {
240
+ from: systemId,
241
+ to: defaultAgentId,
242
+ kind: 'message.system',
243
+ detail: message.content,
244
+ };
245
+ }
246
+ return {
247
+ from: defaultAgentId,
248
+ to: userId,
249
+ kind: 'message.assistant',
250
+ detail: message.content,
251
+ };
252
+ }
253
+ function runtimeEventRoute(event) {
254
+ const type = event['type'];
255
+ const agentName = event['agentName'];
256
+ if (typeof type !== 'string' || typeof agentName !== 'string') {
257
+ return undefined;
258
+ }
259
+ const agentId = `agent:${agentName}`;
260
+ if (type === 'tool.called') {
261
+ const toolName = event['toolName'];
262
+ return {
263
+ from: agentId,
264
+ to: `tool:${typeof toolName === 'string' ? toolName : 'unknown'}`,
265
+ detail: typeof toolName === 'string' ? toolName : '',
266
+ };
267
+ }
268
+ if (type === 'tool.completed' || type === 'tool.failed') {
269
+ const toolName = event['toolName'];
270
+ const status = event['status'];
271
+ const suffix = typeof status === 'string' ? ` (${status})` : '';
272
+ return {
273
+ from: `tool:${typeof toolName === 'string' ? toolName : 'unknown'}`,
274
+ to: agentId,
275
+ detail: `${typeof toolName === 'string' ? toolName : 'unknown'}${suffix}`,
276
+ };
277
+ }
278
+ return {
279
+ from: agentId,
280
+ to: 'system:runtime',
281
+ detail: type,
282
+ };
283
+ }
284
+ function parseConnectorLogLine(line, fallbackOffset) {
285
+ const pattern = /\[goondan-runtime\]\[([^/\]]+)\/([^\]]+)\] emitted event name=([^\s]+) instanceKey=([^\s]+)/u;
286
+ const match = pattern.exec(line);
287
+ if (!match) {
288
+ return undefined;
289
+ }
290
+ const connectionName = match[1] ?? '';
291
+ const connectorName = match[2] ?? '';
292
+ const eventName = match[3] ?? '';
293
+ if (connectionName.length === 0 || connectorName.length === 0 || eventName.length === 0) {
294
+ return undefined;
295
+ }
296
+ const timestampMatch = line.match(/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)/u);
297
+ const at = normalizeTimestamp(timestampMatch ? timestampMatch[1] : undefined, fallbackOffset);
298
+ return {
299
+ at,
300
+ connectionName,
301
+ connectorName,
302
+ eventName,
303
+ };
304
+ }
305
+ async function resolveInstanceLocations(stateRoot, instanceKey) {
306
+ const safeKey = sanitizeInstanceKey(instanceKey);
307
+ const results = [];
308
+ const seen = new Set();
309
+ const workspacesRoot = path.join(stateRoot, 'workspaces');
310
+ if (await exists(workspacesRoot)) {
311
+ const workspaces = await readdir(workspacesRoot, { withFileTypes: true });
312
+ for (const workspace of workspaces) {
313
+ if (!workspace.isDirectory()) {
314
+ continue;
315
+ }
316
+ const workspaceName = workspace.name;
317
+ const candidates = [
318
+ path.join(workspacesRoot, workspaceName, 'instances', safeKey),
319
+ path.join(workspacesRoot, workspaceName, 'instances', instanceKey),
320
+ ];
321
+ for (const candidate of candidates) {
322
+ if (!(await exists(candidate))) {
323
+ continue;
324
+ }
325
+ if (seen.has(candidate)) {
326
+ continue;
327
+ }
328
+ seen.add(candidate);
329
+ results.push({
330
+ workspaceName,
331
+ instancePath: candidate,
332
+ });
333
+ }
334
+ }
335
+ }
336
+ const legacyRoot = path.join(stateRoot, 'instances');
337
+ if (await exists(legacyRoot)) {
338
+ const workspaceCandidates = await readdir(legacyRoot, { withFileTypes: true });
339
+ for (const workspace of workspaceCandidates) {
340
+ if (!workspace.isDirectory()) {
341
+ continue;
342
+ }
343
+ const workspaceName = workspace.name;
344
+ const candidates = [
345
+ path.join(legacyRoot, workspaceName, safeKey),
346
+ path.join(legacyRoot, workspaceName, instanceKey),
347
+ ];
348
+ for (const candidate of candidates) {
349
+ if (!(await exists(candidate))) {
350
+ continue;
351
+ }
352
+ if (seen.has(candidate)) {
353
+ continue;
354
+ }
355
+ seen.add(candidate);
356
+ results.push({
357
+ workspaceName,
358
+ instancePath: candidate,
359
+ });
360
+ }
361
+ }
362
+ }
363
+ return results;
364
+ }
365
+ function parseInstanceMetadataAgent(raw) {
366
+ try {
367
+ const parsed = JSON.parse(raw);
368
+ if (!isObjectRecord(parsed)) {
369
+ return undefined;
370
+ }
371
+ const agentName = parsed['agentName'];
372
+ if (typeof agentName === 'string' && agentName.length > 0) {
373
+ return agentName;
374
+ }
375
+ return undefined;
376
+ }
377
+ catch {
378
+ return undefined;
379
+ }
380
+ }
381
+ function compareTimeline(a, b) {
382
+ if (a.sortAt !== b.sortAt) {
383
+ return a.sortAt - b.sortAt;
384
+ }
385
+ return a.sequence - b.sequence;
386
+ }
387
+ function finalizeParticipants(participants) {
388
+ return [...participants.values()]
389
+ .sort((a, b) => {
390
+ const timeGap = toMillis(b.lastSeenAt) - toMillis(a.lastSeenAt);
391
+ if (timeGap !== 0) {
392
+ return timeGap;
393
+ }
394
+ return a.order - b.order;
395
+ })
396
+ .map((item) => ({
397
+ id: item.id,
398
+ label: item.label,
399
+ kind: item.kind,
400
+ lastSeenAt: item.lastSeenAt,
401
+ }));
402
+ }
403
+ function finalizeInteractions(interactions) {
404
+ return [...interactions.values()]
405
+ .sort((a, b) => toMillis(b.lastSeenAt) - toMillis(a.lastSeenAt))
406
+ .map((item) => ({
407
+ key: item.key,
408
+ a: item.a,
409
+ b: item.b,
410
+ total: item.total,
411
+ lastSeenAt: item.lastSeenAt,
412
+ direction: item.direction,
413
+ history: [...item.history].sort((x, y) => toMillis(x.at) - toMillis(y.at)),
414
+ }));
415
+ }
416
+ function normalizeHostForDisplay(host) {
417
+ if (host === '0.0.0.0' || host === '::') {
418
+ return '127.0.0.1';
419
+ }
420
+ if (host.includes(':') && !host.startsWith('[')) {
421
+ return `[${host}]`;
422
+ }
423
+ return host;
424
+ }
425
+ function parseRecentLimit(value) {
426
+ if (value === null || value.trim().length === 0) {
427
+ return undefined;
428
+ }
429
+ const parsed = Number.parseInt(value, 10);
430
+ if (!Number.isFinite(parsed) || parsed <= 0) {
431
+ return undefined;
432
+ }
433
+ return Math.min(parsed, 200);
434
+ }
435
+ function writeJson(res, statusCode, payload) {
436
+ const body = JSON.stringify(payload);
437
+ res.statusCode = statusCode;
438
+ res.setHeader('content-type', 'application/json; charset=utf-8');
439
+ res.setHeader('cache-control', 'no-store');
440
+ res.end(body);
441
+ }
442
+ function writeText(res, statusCode, contentType, body) {
443
+ res.statusCode = statusCode;
444
+ res.setHeader('content-type', contentType);
445
+ res.setHeader('cache-control', 'no-store');
446
+ res.end(body);
447
+ }
448
+ function notFound(res) {
449
+ writeJson(res, 404, {
450
+ error: 'not_found',
451
+ message: '요청한 리소스를 찾을 수 없습니다.',
452
+ });
453
+ }
454
+ export class DefaultStudioService {
455
+ env;
456
+ instances;
457
+ constructor(env, instances) {
458
+ this.env = env;
459
+ this.instances = instances;
460
+ }
461
+ async listInstances(request) {
462
+ const rows = await this.instances.list({
463
+ limit: 200,
464
+ all: true,
465
+ stateRoot: request.stateRoot,
466
+ });
467
+ return rows.map((row) => ({
468
+ key: row.key,
469
+ status: row.status,
470
+ agent: row.agent,
471
+ createdAt: row.createdAt,
472
+ updatedAt: row.updatedAt,
473
+ }));
474
+ }
475
+ async loadVisualization(request) {
476
+ const stateRoot = resolveStateRoot(request.stateRoot, this.env);
477
+ const instanceLocations = await resolveInstanceLocations(stateRoot, request.instanceKey);
478
+ const participants = new Map();
479
+ const interactions = new Map();
480
+ const timeline = [];
481
+ let sequence = 0;
482
+ const defaultAgentId = 'agent:orchestrator';
483
+ registerParticipant(participants, defaultAgentId, 'orchestrator', 'agent', nowIso());
484
+ registerParticipant(participants, `user:${request.instanceKey}`, 'user', 'user', nowIso(1));
485
+ const knownAgents = new Set(['orchestrator']);
486
+ for (const location of instanceLocations) {
487
+ const metadataPath = path.join(location.instancePath, 'metadata.json');
488
+ const metadataRaw = await readTextFileIfExists(metadataPath);
489
+ const metadataAgent = metadataRaw ? parseInstanceMetadataAgent(metadataRaw) : undefined;
490
+ if (metadataAgent) {
491
+ knownAgents.add(metadataAgent);
492
+ registerParticipant(participants, `agent:${metadataAgent}`, metadataAgent, 'agent', nowIso(sequence));
493
+ }
494
+ const basePath = path.join(location.instancePath, 'messages', 'base.jsonl');
495
+ const eventsPath = path.join(location.instancePath, 'messages', 'events.jsonl');
496
+ const runtimeEventsPath = path.join(location.instancePath, 'messages', 'runtime-events.jsonl');
497
+ const baseRows = await readJsonLines(basePath);
498
+ for (const row of baseRows) {
499
+ const message = messageFromUnknown(row);
500
+ if (!message) {
501
+ continue;
502
+ }
503
+ const at = normalizeTimestamp(message.createdAt, sequence);
504
+ const principalAgent = metadataAgent ?? 'orchestrator';
505
+ const principalAgentId = `agent:${principalAgent}`;
506
+ registerParticipant(participants, principalAgentId, principalAgent, 'agent', at);
507
+ const routed = fromMessageToRoute(message, request.instanceKey, principalAgentId);
508
+ registerParticipant(participants, routed.from, routed.from.replace(/^[^:]+:/u, ''), classifyParticipantKind(message.sourceType), at);
509
+ registerParticipant(participants, routed.to, routed.to.replace(/^[^:]+:/u, ''), routed.to.startsWith('agent:') ? 'agent' : 'unknown', at);
510
+ registerInteraction(interactions, routed.from, routed.to, at, routed.kind, routed.detail);
511
+ pushTimeline(timeline, {
512
+ at,
513
+ kind: 'message',
514
+ source: routed.from,
515
+ target: routed.to,
516
+ subtype: routed.kind,
517
+ detail: routed.detail,
518
+ }, sequence);
519
+ sequence += 1;
520
+ }
521
+ const eventRows = await readJsonLines(eventsPath);
522
+ for (const row of eventRows) {
523
+ if (!isObjectRecord(row)) {
524
+ continue;
525
+ }
526
+ const eventType = row['type'];
527
+ if (typeof eventType !== 'string') {
528
+ continue;
529
+ }
530
+ if (eventType === 'append') {
531
+ const message = messageFromUnknown(row['message']);
532
+ if (!message) {
533
+ continue;
534
+ }
535
+ const at = normalizeTimestamp(message.createdAt, sequence);
536
+ const principalAgent = metadataAgent ?? 'orchestrator';
537
+ const principalAgentId = `agent:${principalAgent}`;
538
+ const routed = fromMessageToRoute(message, request.instanceKey, principalAgentId);
539
+ registerParticipant(participants, routed.from, routed.from.replace(/^[^:]+:/u, ''), classifyParticipantKind(message.sourceType), at);
540
+ registerParticipant(participants, routed.to, routed.to.replace(/^[^:]+:/u, ''), routed.to.startsWith('agent:') ? 'agent' : 'unknown', at);
541
+ registerInteraction(interactions, routed.from, routed.to, at, 'message.append', routed.detail);
542
+ pushTimeline(timeline, {
543
+ at,
544
+ kind: 'message',
545
+ source: routed.from,
546
+ target: routed.to,
547
+ subtype: 'message.append',
548
+ detail: routed.detail,
549
+ }, sequence);
550
+ sequence += 1;
551
+ continue;
552
+ }
553
+ const at = nowIso(sequence);
554
+ pushTimeline(timeline, {
555
+ at,
556
+ kind: 'message',
557
+ source: 'system:runtime',
558
+ target: metadataAgent ? `agent:${metadataAgent}` : defaultAgentId,
559
+ subtype: `event.${eventType}`,
560
+ detail: `message event: ${eventType}`,
561
+ }, sequence);
562
+ sequence += 1;
563
+ }
564
+ const runtimeRows = await readJsonLines(runtimeEventsPath);
565
+ for (const row of runtimeRows) {
566
+ if (!isObjectRecord(row)) {
567
+ continue;
568
+ }
569
+ const at = normalizeTimestamp(row['timestamp'], sequence);
570
+ const routed = runtimeEventRoute(row);
571
+ if (!routed) {
572
+ continue;
573
+ }
574
+ const sourceLabel = routed.from.replace(/^[^:]+:/u, '');
575
+ const targetLabel = routed.to.replace(/^[^:]+:/u, '');
576
+ registerParticipant(participants, routed.from, sourceLabel, routed.from.startsWith('agent:') ? 'agent' : routed.from.startsWith('tool:') ? 'tool' : 'system', at);
577
+ registerParticipant(participants, routed.to, targetLabel, routed.to.startsWith('agent:') ? 'agent' : routed.to.startsWith('tool:') ? 'tool' : 'system', at);
578
+ const subtype = typeof row['type'] === 'string' ? row['type'] : 'runtime.event';
579
+ registerInteraction(interactions, routed.from, routed.to, at, subtype, routed.detail);
580
+ pushTimeline(timeline, {
581
+ at,
582
+ kind: 'runtime-event',
583
+ source: routed.from,
584
+ target: routed.to,
585
+ subtype,
586
+ detail: routed.detail,
587
+ }, sequence);
588
+ sequence += 1;
589
+ }
590
+ }
591
+ const connectorEvents = await this.readConnectorEventsFromLogs(stateRoot, request.instanceKey);
592
+ const primaryAgent = knownAgents.values().next().value ?? 'orchestrator';
593
+ const primaryAgentId = `agent:${primaryAgent}`;
594
+ for (const event of connectorEvents) {
595
+ const source = `connector:${event.connectorName}`;
596
+ const target = primaryAgentId;
597
+ const detail = `${event.connectionName}/${event.connectorName} -> ${event.eventName}`;
598
+ registerParticipant(participants, source, event.connectorName, 'connector', event.at);
599
+ registerParticipant(participants, target, primaryAgent, 'agent', event.at);
600
+ registerInteraction(interactions, source, target, event.at, 'connector.emitted', detail);
601
+ pushTimeline(timeline, {
602
+ at: event.at,
603
+ kind: 'connector-log',
604
+ source,
605
+ target,
606
+ subtype: 'connector.emitted',
607
+ detail,
608
+ }, sequence);
609
+ sequence += 1;
610
+ }
611
+ timeline.sort(compareTimeline);
612
+ const entries = timeline.map((item) => item.entry);
613
+ const recentLimit = request.maxRecentEvents ?? 20;
614
+ const recentEvents = entries.slice(Math.max(0, entries.length - recentLimit));
615
+ return {
616
+ instanceKey: request.instanceKey,
617
+ participants: finalizeParticipants(participants),
618
+ interactions: finalizeInteractions(interactions),
619
+ timeline: entries,
620
+ recentEvents,
621
+ };
622
+ }
623
+ async startServer(request) {
624
+ const host = request.host;
625
+ const port = request.port;
626
+ const stateRoot = resolveStateRoot(request.stateRoot, this.env);
627
+ const server = createServer(async (req, res) => {
628
+ await this.handleHttpRequest(req, res, stateRoot);
629
+ });
630
+ await new Promise((resolve, reject) => {
631
+ server.once('error', reject);
632
+ server.listen(port, host, () => {
633
+ server.off('error', reject);
634
+ resolve();
635
+ });
636
+ });
637
+ const closed = new Promise((resolve) => {
638
+ server.once('close', () => {
639
+ resolve();
640
+ });
641
+ });
642
+ const close = async () => await new Promise((resolve, reject) => {
643
+ server.close((error) => {
644
+ if (error) {
645
+ reject(error);
646
+ return;
647
+ }
648
+ resolve(undefined);
649
+ });
650
+ });
651
+ const addr = server.address();
652
+ const boundPort = typeof addr === 'object' && addr && typeof addr.port === 'number' ? addr.port : port;
653
+ const displayHost = normalizeHostForDisplay(host);
654
+ return {
655
+ url: `http://${displayHost}:${String(boundPort)}`,
656
+ close,
657
+ closed,
658
+ };
659
+ }
660
+ async handleHttpRequest(req, res, stateRoot) {
661
+ const method = req.method ?? 'GET';
662
+ const url = new URL(req.url ?? '/', 'http://studio.local');
663
+ const pathname = url.pathname;
664
+ if (pathname === '/favicon.ico') {
665
+ res.statusCode = 204;
666
+ res.end();
667
+ return;
668
+ }
669
+ if (method === 'GET' && pathname === '/') {
670
+ writeText(res, 200, 'text/html; charset=utf-8', STUDIO_HTML);
671
+ return;
672
+ }
673
+ if (method === 'GET' && pathname === '/studio.css') {
674
+ writeText(res, 200, 'text/css; charset=utf-8', STUDIO_CSS);
675
+ return;
676
+ }
677
+ if (method === 'GET' && pathname === '/studio.js') {
678
+ writeText(res, 200, 'text/javascript; charset=utf-8', STUDIO_JS);
679
+ return;
680
+ }
681
+ const segments = pathname
682
+ .split('/')
683
+ .map((segment) => segment.trim())
684
+ .filter((segment) => segment.length > 0);
685
+ if (method === 'GET' && segments.length === 2 && segments[0] === 'api' && segments[1] === 'instances') {
686
+ const items = await this.listInstances({ stateRoot });
687
+ writeJson(res, 200, {
688
+ items,
689
+ polledAt: new Date().toISOString(),
690
+ });
691
+ return;
692
+ }
693
+ if (method === 'GET' &&
694
+ segments.length === 4 &&
695
+ segments[0] === 'api' &&
696
+ segments[1] === 'instances' &&
697
+ segments[3] === 'visualization') {
698
+ const encodedKey = segments[2];
699
+ const instanceKey = decodeURIComponent(encodedKey ?? '');
700
+ if (!instanceKey) {
701
+ writeJson(res, 400, {
702
+ error: 'invalid_instance_key',
703
+ message: 'instance key가 비어 있습니다.',
704
+ });
705
+ return;
706
+ }
707
+ const maxRecentEvents = parseRecentLimit(url.searchParams.get('recent'));
708
+ const visualization = await this.loadVisualization({
709
+ stateRoot,
710
+ instanceKey,
711
+ maxRecentEvents,
712
+ });
713
+ writeJson(res, 200, visualization);
714
+ return;
715
+ }
716
+ notFound(res);
717
+ }
718
+ async readConnectorEventsFromLogs(stateRoot, instanceKey) {
719
+ const events = [];
720
+ const logDir = path.join(stateRoot, 'runtime', 'logs', instanceKey);
721
+ if (!(await exists(logDir))) {
722
+ return events;
723
+ }
724
+ const files = await readdir(logDir, { withFileTypes: true });
725
+ let sequence = 0;
726
+ for (const file of files) {
727
+ if (!file.isFile() || !file.name.endsWith('.log')) {
728
+ continue;
729
+ }
730
+ const fullPath = path.join(logDir, file.name);
731
+ const content = await readTextFileIfExists(fullPath);
732
+ if (!content) {
733
+ continue;
734
+ }
735
+ const stats = await stat(fullPath);
736
+ const baseOffset = stats.mtime.getTime();
737
+ const lines = content.split('\n').map((line) => line.trim()).filter((line) => line.length > 0);
738
+ for (const line of lines) {
739
+ const parsed = parseConnectorLogLine(line, sequence + Math.max(1, Math.trunc(baseOffset / 1000)));
740
+ if (!parsed) {
741
+ sequence += 1;
742
+ continue;
743
+ }
744
+ events.push(parsed);
745
+ sequence += 1;
746
+ }
747
+ }
748
+ return events.sort((a, b) => toMillis(a.at) - toMillis(b.at));
749
+ }
750
+ }
751
+ //# sourceMappingURL=studio.js.map