prism-debugger 0.2.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/src/broker.js ADDED
@@ -0,0 +1,425 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ import { logger } from './logger.js';
3
+
4
+ const ALLOWED_LEVELS = new Set(['debug', 'info', 'warn', 'error', 'perfwarning']);
5
+
6
+ const nowIso = () => new Date().toISOString();
7
+
8
+ const safeJsonParse = (raw) => {
9
+ try {
10
+ return JSON.parse(raw);
11
+ } catch {
12
+ return null;
13
+ }
14
+ };
15
+
16
+ const extractToken = (req) => {
17
+ const queryToken = new URL(req.url, 'http://localhost').searchParams.get('token');
18
+ if (queryToken) return queryToken;
19
+
20
+ const auth = req.headers.authorization ?? '';
21
+ if (auth.toLowerCase().startsWith('bearer ')) {
22
+ return auth.slice(7).trim();
23
+ }
24
+ return '';
25
+ };
26
+
27
+ const getPeer = (req) => req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress || 'unknown';
28
+
29
+ export class Broker {
30
+ constructor({ server, config, storage, pluginManager }) {
31
+ this.config = config;
32
+ this.storage = storage;
33
+ this.pluginManager = pluginManager;
34
+
35
+ this.iosSockets = new Set();
36
+ this.uiSockets = new Set();
37
+
38
+ this.iosSocketByDebuggerId = new Map();
39
+ this.lastHeartbeatByDebuggerId = new Map();
40
+ this.rateStateByDebuggerId = new Map();
41
+
42
+ this.wssIos = new WebSocketServer({ noServer: true });
43
+ this.wssUi = new WebSocketServer({ noServer: true });
44
+
45
+ server.on('upgrade', (req, socket, head) => {
46
+ if (req.url?.startsWith('/ws/ios')) {
47
+ this.wssIos.handleUpgrade(req, socket, head, (ws) => {
48
+ this.wssIos.emit('connection', ws, req);
49
+ });
50
+ return;
51
+ }
52
+
53
+ if (req.url?.startsWith('/ws/ui')) {
54
+ this.wssUi.handleUpgrade(req, socket, head, (ws) => {
55
+ this.wssUi.emit('connection', ws, req);
56
+ });
57
+ return;
58
+ }
59
+
60
+ socket.destroy();
61
+ });
62
+
63
+ this.wssIos.on('connection', (ws, req) => this.onIosConnection(ws, req));
64
+ this.wssUi.on('connection', (ws, req) => this.onUiConnection(ws, req));
65
+
66
+ this.heartbeatInterval = setInterval(() => this.reapHeartbeatTimeouts(), 1000);
67
+ }
68
+
69
+ stop() {
70
+ clearInterval(this.heartbeatInterval);
71
+
72
+ this.broadcastUi({ type: 'server.shutdown', ts: nowIso() });
73
+
74
+ for (const ws of this.iosSockets) {
75
+ ws.close(1001, 'server shutting down');
76
+ }
77
+ for (const ws of this.uiSockets) {
78
+ ws.close(1001, 'server shutting down');
79
+ }
80
+
81
+ this.wssIos.close();
82
+ this.wssUi.close();
83
+ }
84
+
85
+ onIosConnection(ws, req) {
86
+ const token = extractToken(req);
87
+ if (!this.config.iosAuthKeys.has(token)) {
88
+ ws.close(1008, 'unauthorized');
89
+ return;
90
+ }
91
+
92
+ const peer = getPeer(req);
93
+ ws.meta = { peer, debuggerIds: new Set() };
94
+ this.iosSockets.add(ws);
95
+ logger.ios('connect', peer);
96
+
97
+ ws.on('message', (raw) => this.onIosMessage(ws, raw));
98
+ ws.on('close', () => this.onIosClose(ws));
99
+ ws.on('error', () => this.onIosClose(ws));
100
+
101
+ this.safeSend(ws, {
102
+ type: 'system.ack',
103
+ ts: nowIso(),
104
+ message: 'ios socket connected'
105
+ });
106
+ }
107
+
108
+ onUiConnection(ws, req) {
109
+ const token = extractToken(req);
110
+ if (!this.config.uiAuthKeys.has(token)) {
111
+ ws.close(1008, 'unauthorized');
112
+ return;
113
+ }
114
+
115
+ this.uiSockets.add(ws);
116
+ logger.ui('connect', this.uiSockets.size);
117
+ ws.on('message', (raw) => this.onUiMessage(ws, raw));
118
+ ws.on('close', () => { this.uiSockets.delete(ws); logger.ui('disconnect', this.uiSockets.size); });
119
+ ws.on('error', () => { this.uiSockets.delete(ws); });
120
+
121
+ this.safeSend(ws, {
122
+ type: 'debuggers.snapshot',
123
+ ts: nowIso(),
124
+ debuggers: this.storage.listDebuggers()
125
+ });
126
+ }
127
+
128
+ onIosMessage(ws, raw) {
129
+ const text = raw.toString();
130
+ if (Buffer.byteLength(text, 'utf8') > this.config.maxPayloadBytes) {
131
+ this.safeSend(ws, {
132
+ type: 'system.error',
133
+ ts: nowIso(),
134
+ error: `payload too large (${this.config.maxPayloadBytes} bytes max)`
135
+ });
136
+ return;
137
+ }
138
+
139
+ const msg = safeJsonParse(text);
140
+ if (!msg || typeof msg !== 'object') {
141
+ return;
142
+ }
143
+
144
+ const err = this.validateIosEvent(msg);
145
+ if (err) {
146
+ this.safeSend(ws, { type: 'system.error', ts: nowIso(), error: err });
147
+ return;
148
+ }
149
+
150
+ if (!this.checkRateLimit(msg.debuggerId)) {
151
+ this.safeSend(ws, {
152
+ type: 'system.error',
153
+ ts: nowIso(),
154
+ error: `rate limit exceeded for ${msg.debuggerId}`
155
+ });
156
+ return;
157
+ }
158
+
159
+ this.acceptIosEvent(ws, msg);
160
+ }
161
+
162
+ onUiMessage(ws, raw) {
163
+ const msg = safeJsonParse(raw.toString());
164
+ if (!msg || typeof msg !== 'object') return;
165
+
166
+ if (msg.action === 'list') {
167
+ this.safeSend(ws, {
168
+ type: 'debuggers.snapshot',
169
+ ts: nowIso(),
170
+ debuggers: this.storage.listDebuggers()
171
+ });
172
+ return;
173
+ }
174
+
175
+ if (msg.action === 'history' && typeof msg.debuggerId === 'string') {
176
+ this.safeSend(ws, {
177
+ type: 'messages.history',
178
+ ts: nowIso(),
179
+ debuggerId: msg.debuggerId,
180
+ messages: this.storage.getMessages(msg.debuggerId)
181
+ });
182
+ return;
183
+ }
184
+
185
+ if (msg.action === 'perfpoints.history' && typeof msg.debuggerId === 'string') {
186
+ this.safeSend(ws, {
187
+ type: 'perfpoints.history',
188
+ ts: nowIso(),
189
+ debuggerId: msg.debuggerId,
190
+ points: this.storage.getPerfPoints(msg.debuggerId),
191
+ specs: this.storage.getPerfPointSpecs(msg.debuggerId)
192
+ });
193
+ return;
194
+ }
195
+
196
+ if (msg.action === 'clear-inactive') {
197
+ const removed = this.storage.removeOfflineDebuggers();
198
+ for (const id of removed) {
199
+ this.lastHeartbeatByDebuggerId.delete(id);
200
+ this.iosSocketByDebuggerId.delete(id);
201
+ this.rateStateByDebuggerId.delete(id);
202
+ }
203
+ this.broadcastUi({
204
+ type: 'debuggers.snapshot',
205
+ ts: nowIso(),
206
+ debuggers: this.storage.listDebuggers()
207
+ });
208
+ return;
209
+ }
210
+
211
+ if (msg.action === 'send' && typeof msg.debuggerId === 'string') {
212
+ const ok = this.sendToDebugger(msg);
213
+ if (!ok) {
214
+ this.safeSend(ws, {
215
+ type: 'system.error',
216
+ ts: nowIso(),
217
+ error: `debugger ${msg.debuggerId} is offline`
218
+ });
219
+ }
220
+ return;
221
+ }
222
+ }
223
+
224
+ sendToDebugger(command) {
225
+ const target = this.iosSocketByDebuggerId.get(command.debuggerId);
226
+ if (!target || target.readyState !== WebSocket.OPEN) {
227
+ return false;
228
+ }
229
+ const known = this.storage.getDebugger(command.debuggerId);
230
+
231
+ const outbound = {
232
+ v: 1,
233
+ type: 'event',
234
+ ts: nowIso(),
235
+ direction: 'server->ios',
236
+ debuggerId: command.debuggerId,
237
+ sessionId: command.sessionId ?? known?.sessionId ?? null,
238
+ eventName: command.eventName ?? 'command.execute',
239
+ level: command.level ?? 'info',
240
+ category: command.category ?? 'command',
241
+ correlationId: command.correlationId ?? null,
242
+ payload: command.payload ?? {}
243
+ };
244
+
245
+ this.safeSend(target, outbound);
246
+ this.pluginManager?.emit('onServerEventSent', {
247
+ ts: outbound.ts,
248
+ message: outbound
249
+ });
250
+ return true;
251
+ }
252
+
253
+ validateIosEvent(msg) {
254
+ if (msg.v !== 1) return 'unsupported protocol version';
255
+ if (msg.type !== 'event') return 'invalid message type';
256
+ if (typeof msg.debuggerId !== 'string' || msg.debuggerId.length < 5) {
257
+ return 'invalid debuggerId';
258
+ }
259
+ if (typeof msg.eventName !== 'string' || !msg.eventName.length) {
260
+ return 'eventName is required';
261
+ }
262
+ if (msg.level && !ALLOWED_LEVELS.has(msg.level)) {
263
+ return 'invalid level';
264
+ }
265
+ return null;
266
+ }
267
+
268
+ checkRateLimit(debuggerId) {
269
+ const now = Date.now();
270
+ const state = this.rateStateByDebuggerId.get(debuggerId) ?? { bucketStartMs: now, count: 0 };
271
+
272
+ if (now - state.bucketStartMs >= 1000) {
273
+ state.bucketStartMs = now;
274
+ state.count = 0;
275
+ }
276
+
277
+ state.count += 1;
278
+ this.rateStateByDebuggerId.set(debuggerId, state);
279
+ return state.count <= this.config.rateLimit;
280
+ }
281
+
282
+ acceptIosEvent(ws, msg) {
283
+ const receivedTs = nowIso();
284
+ const debuggerId = msg.debuggerId;
285
+
286
+ ws.meta.debuggerIds.add(debuggerId);
287
+ this.iosSocketByDebuggerId.set(debuggerId, ws);
288
+
289
+ const existing = this.storage.getDebugger(debuggerId);
290
+ if (msg.eventName === 'debugger.register' || !existing) {
291
+ const payload = msg.payload ?? {};
292
+ const name = payload.name ?? `Debugger ${debuggerId.slice(0, 8)}`;
293
+ this.storage.upsertDebugger({
294
+ debuggerId,
295
+ name,
296
+ tags: payload.tags ?? {},
297
+ createdAt: payload.createdAt ?? receivedTs,
298
+ status: 'online',
299
+ lastSeen: receivedTs,
300
+ app: payload.app ?? {},
301
+ sessionId: msg.sessionId ?? null
302
+ });
303
+ if (msg.eventName === 'debugger.register') {
304
+ logger.register(debuggerId, name);
305
+ }
306
+ } else {
307
+ this.storage.upsertDebugger({
308
+ ...existing,
309
+ status: 'online',
310
+ lastSeen: receivedTs
311
+ });
312
+ }
313
+
314
+ if (msg.eventName === 'debugger.unregister') {
315
+ this.storage.setDebuggerOffline(debuggerId, receivedTs);
316
+ this.lastHeartbeatByDebuggerId.delete(debuggerId);
317
+ }
318
+
319
+ if (msg.eventName === 'debugger.ping') {
320
+ this.lastHeartbeatByDebuggerId.set(debuggerId, Date.now());
321
+ const dbg = this.storage.getDebugger(debuggerId);
322
+ if (dbg) {
323
+ this.storage.upsertDebugger({ ...dbg, status: 'online', lastSeen: receivedTs });
324
+ }
325
+ }
326
+
327
+ const debuggerState = this.storage.getDebugger(debuggerId);
328
+
329
+ if (msg.eventName === 'debugger.perfpoints.register') {
330
+ const specs = msg.payload?.specs ?? [];
331
+ this.storage.setPerfPointSpecs(debuggerId, specs);
332
+ this.broadcastUi({ type: 'perfpoints.specs', ts: receivedTs, debuggerId, specs });
333
+ return;
334
+ }
335
+
336
+ if (msg.eventName === 'debugger.perfpoint') {
337
+ const data = msg.payload?.message ?? {};
338
+ const STANDARD_FIELDS = new Set(['id', 'badge', 'label', 'startTs', 'endTs', 'duration', 'contextId']);
339
+ const extra = Object.fromEntries(Object.entries(data).filter(([k]) => !STANDARD_FIELDS.has(k)));
340
+ const point = {
341
+ id: data.id ?? '',
342
+ badge: data.badge ?? '',
343
+ label: data.label ?? '',
344
+ startTs: data.startTs ?? receivedTs,
345
+ endTs: data.endTs ?? receivedTs,
346
+ duration: data.duration ?? 0,
347
+ contextId: msg.payload?.contextId ?? data.contextId ?? '',
348
+ extra: Object.keys(extra).length > 0 ? extra : undefined,
349
+ receivedTs
350
+ };
351
+ this.storage.addPerfPoint(debuggerId, point);
352
+ this.broadcastUi({ type: 'perfpoint', ts: receivedTs, debuggerId, point });
353
+ logger.perfpoint(debuggerId, point.badge, point.label, point.duration);
354
+ } else {
355
+ if (msg.eventName !== 'debugger.register' && msg.eventName !== 'debugger.ping' && msg.eventName !== 'debugger.unregister') {
356
+ logger.event(msg.level ?? 'info', debuggerId, msg.eventName, msg.payload?.contextId);
357
+ }
358
+ this.storage.addMessage(debuggerId, msg);
359
+ this.pluginManager?.emit('onIosEventAccepted', {
360
+ ts: receivedTs,
361
+ message: msg,
362
+ debuggerState,
363
+ peer: ws.meta?.peer ?? 'unknown'
364
+ });
365
+ this.broadcastUi({ type: 'message', ts: receivedTs, message: msg });
366
+ }
367
+
368
+ this.broadcastUi({
369
+ type: 'debugger.update',
370
+ ts: receivedTs,
371
+ debugger: debuggerState
372
+ });
373
+ }
374
+
375
+ reapHeartbeatTimeouts() {
376
+ const nowMs = Date.now();
377
+ const timeoutMs = this.config.heartbeatTimeoutSec * 1000;
378
+
379
+ for (const [debuggerId, lastPing] of this.lastHeartbeatByDebuggerId.entries()) {
380
+ if (nowMs - lastPing <= timeoutMs) continue;
381
+
382
+ this.lastHeartbeatByDebuggerId.delete(debuggerId);
383
+ this.storage.setDebuggerOffline(debuggerId, nowIso());
384
+ this.broadcastUi({
385
+ type: 'debugger.update',
386
+ ts: nowIso(),
387
+ debugger: this.storage.getDebugger(debuggerId)
388
+ });
389
+ }
390
+ }
391
+
392
+ onIosClose(ws) {
393
+ if (!this.iosSockets.has(ws)) return;
394
+ this.iosSockets.delete(ws);
395
+ logger.ios('disconnect', ws.meta?.peer ?? 'unknown');
396
+
397
+ const now = nowIso();
398
+ for (const debuggerId of ws.meta.debuggerIds ?? []) {
399
+ const bound = this.iosSocketByDebuggerId.get(debuggerId);
400
+ if (bound === ws) {
401
+ this.iosSocketByDebuggerId.delete(debuggerId);
402
+ }
403
+ this.storage.setDebuggerOffline(debuggerId, now);
404
+ this.broadcastUi({
405
+ type: 'debugger.update',
406
+ ts: now,
407
+ debugger: this.storage.getDebugger(debuggerId)
408
+ });
409
+ }
410
+ }
411
+
412
+ broadcastUi(payload) {
413
+ const raw = JSON.stringify(payload);
414
+ for (const ws of this.uiSockets) {
415
+ if (ws.readyState === WebSocket.OPEN) {
416
+ ws.send(raw);
417
+ }
418
+ }
419
+ }
420
+
421
+ safeSend(ws, payload) {
422
+ if (ws.readyState !== WebSocket.OPEN) return;
423
+ ws.send(JSON.stringify(payload));
424
+ }
425
+ }
package/src/config.js ADDED
@@ -0,0 +1,55 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+
7
+ const envPath = path.resolve(process.cwd(), '.env');
8
+ if (typeof process.loadEnvFile === 'function') {
9
+ try { process.loadEnvFile(envPath); } catch {}
10
+ }
11
+
12
+ const parseSet = (value, fallback) => {
13
+ const input = value ?? fallback;
14
+ return new Set(
15
+ String(input)
16
+ .split(',')
17
+ .map((x) => x.trim())
18
+ .filter(Boolean)
19
+ );
20
+ };
21
+
22
+ const parseNumber = (value, fallback) => {
23
+ const parsed = Number(value);
24
+ return Number.isFinite(parsed) ? parsed : fallback;
25
+ };
26
+
27
+ const parseList = (value, fallback) => {
28
+ const input = value === undefined ? fallback : value;
29
+ return String(input)
30
+ .split(',')
31
+ .map((x) => x.trim())
32
+ .filter(Boolean);
33
+ };
34
+
35
+ const resolvePathFromEnv = (value, fallbackPath) => {
36
+ if (!value) {
37
+ return fallbackPath;
38
+ }
39
+ return path.resolve(process.cwd(), value);
40
+ };
41
+
42
+ export const config = {
43
+ port: parseNumber(process.env.PORT, 8080),
44
+ iosAuthKeys: parseSet(process.env.IOS_AUTH_KEYS, 'ios-dev-token'),
45
+ uiAuthKeys: parseSet(process.env.UI_AUTH_KEYS, 'ui-dev-token'),
46
+ heartbeatTimeoutSec: parseNumber(process.env.HEARTBEAT_TIMEOUT_SEC, 30),
47
+ rateLimit: parseNumber(process.env.RATE_LIMIT, 30),
48
+ maxPayloadBytes: parseNumber(process.env.MAX_PAYLOAD_BYTES, 262144),
49
+ ringBufferSize: parseNumber(process.env.RING_BUFFER_SIZE, 500),
50
+ enabledPlugins: parseList(process.env.ENABLED_PLUGINS, 'context-device-event-logger'),
51
+ contextEventLogDir: resolvePathFromEnv(
52
+ process.env.CONTEXT_EVENT_LOG_DIR,
53
+ path.resolve(__dirname, '../logs/context-events')
54
+ )
55
+ };
package/src/index.js ADDED
@@ -0,0 +1,100 @@
1
+ import http from 'http';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { readFileSync } from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+ import express from 'express';
7
+ import { config } from './config.js';
8
+ import { InMemoryStorage } from './storage.js';
9
+ import { Broker } from './broker.js';
10
+ import { createPluginsFromConfig } from './plugins/index.js';
11
+ import { PluginManager } from './plugins/plugin-manager.js';
12
+ import { logger } from './logger.js';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ const app = express();
18
+ app.use(express.json({ limit: `${config.maxPayloadBytes}b` }));
19
+
20
+ const storage = new InMemoryStorage({ ringBufferSize: config.ringBufferSize });
21
+ const plugins = createPluginsFromConfig(config);
22
+ const pluginManager = new PluginManager({ plugins });
23
+
24
+ function getLocalIP() {
25
+ const nets = os.networkInterfaces();
26
+ for (const name of Object.keys(nets)) {
27
+ for (const net of nets[name]) {
28
+ if (net.family === 'IPv4' && !net.internal) return net.address;
29
+ }
30
+ }
31
+ return 'localhost';
32
+ }
33
+
34
+ app.get('/health', (_req, res) => {
35
+ res.json({ ok: true, ts: new Date().toISOString() });
36
+ });
37
+
38
+ app.get('/api/connect-info', (_req, res) => {
39
+ const ip = getLocalIP();
40
+ res.json({ wsLink: `ws://${ip}:${config.port}` });
41
+ });
42
+
43
+ app.get('/api/debuggers', (_req, res) => {
44
+ res.json({ items: storage.listDebuggers() });
45
+ });
46
+
47
+ app.get('/api/debuggers/:id/messages', (req, res) => {
48
+ res.json({ items: storage.getMessages(req.params.id) });
49
+ });
50
+
51
+ app.post('/api/debuggers/:id/send', (req, res) => {
52
+ const ok = req.app.locals.broker.sendToDebugger({
53
+ debuggerId: req.params.id,
54
+ eventName: req.body.eventName,
55
+ payload: req.body.payload,
56
+ level: req.body.level,
57
+ category: req.body.category,
58
+ correlationId: req.body.correlationId
59
+ });
60
+
61
+ if (!ok) {
62
+ res.status(409).json({ ok: false, error: 'debugger is offline' });
63
+ return;
64
+ }
65
+
66
+ res.json({ ok: true, sent: true });
67
+ });
68
+
69
+ app.use('/', express.static(path.resolve(__dirname, '../public')));
70
+
71
+ const server = http.createServer(app);
72
+ const broker = new Broker({ server, config, storage, pluginManager });
73
+ app.locals.broker = broker;
74
+
75
+ await pluginManager.init({ config, storage });
76
+
77
+ const envFile = path.resolve(process.cwd(), '.env');
78
+ const envLoaded = (await import('fs')).existsSync(envFile);
79
+ const pkg = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf8'));
80
+
81
+ server.listen(config.port, () => {
82
+ const ip = getLocalIP();
83
+ logger.banner({
84
+ version: pkg.version,
85
+ configPath: envLoaded ? envFile : 'defaults (no .env, run prism init)',
86
+ httpUrl: `http://localhost:${config.port}`,
87
+ uiUrl: `http://${ip}:${config.port}/?token=${[...config.uiAuthKeys][0]}`,
88
+ wsIosUrl: `ws://${ip}:${config.port}/ws/ios`,
89
+ plugins: plugins.length > 0 ? pluginManager.listNames().join(', ') : 'none',
90
+ });
91
+ });
92
+
93
+ process.on('SIGINT', () => {
94
+ Promise.resolve()
95
+ .then(() => pluginManager.stop({ config, storage }))
96
+ .finally(() => {
97
+ broker.stop();
98
+ server.close(() => process.exit(0));
99
+ });
100
+ });
package/src/logger.js ADDED
@@ -0,0 +1,72 @@
1
+ const t = process.stdout.isTTY;
2
+
3
+ const c = {
4
+ r: t ? '\x1b[0m' : '',
5
+ b: t ? '\x1b[1m' : '',
6
+ d: t ? '\x1b[2m' : '',
7
+ red: t ? '\x1b[31m' : '',
8
+ grn: t ? '\x1b[32m' : '',
9
+ ylw: t ? '\x1b[33m' : '',
10
+ blu: t ? '\x1b[34m' : '',
11
+ mag: t ? '\x1b[35m' : '',
12
+ cyn: t ? '\x1b[36m' : '',
13
+ gry: t ? '\x1b[90m' : '',
14
+ };
15
+
16
+ const ts = () => new Date().toLocaleTimeString('en-GB', { hour12: false });
17
+
18
+ const LEVEL = {
19
+ debug: { color: c.gry, tag: 'DBG' },
20
+ info: { color: c.cyn, tag: 'INF' },
21
+ warn: { color: c.ylw, tag: 'WRN' },
22
+ error: { color: c.red, tag: 'ERR' },
23
+ perfwarning: { color: c.mag, tag: 'PRF' },
24
+ };
25
+
26
+ export const logger = {
27
+ banner({ version, configPath, httpUrl, uiUrl, wsIosUrl, plugins }) {
28
+ const R = c.r, B = c.b, D = c.d, G = c.gry;
29
+ const spec = `${c.red}━━${R}${c.ylw}━━${R}${c.grn}━━${R}${c.cyn}━━${R}${c.blu}━━${R}${c.mag}━━${R}`;
30
+ console.log(`
31
+ ${G} △${R}
32
+ ${G} ╱ ╲${R} ${spec}
33
+ ${G} ╱ ╲${R} ${B}PRISM${R} ${D}v${version}${R}
34
+ ${G} ╱ ╲${R} ${spec}
35
+ ${G} ╱───────╲${R}
36
+
37
+ ${D}Config${R} ${configPath}
38
+ ${D}HTTP${R} ${c.cyn}${httpUrl}${R}
39
+ ${D}UI${R} ${c.cyn}${uiUrl}${R}
40
+ ${D}WS iOS${R} ${D}${wsIosUrl}${R}
41
+ ${D}Plugins${R} ${plugins}
42
+ `);
43
+ },
44
+
45
+ ios(action, info) {
46
+ const dot = action === 'connect' ? `${c.grn}●${c.r}` : `${c.red}○${c.r}`;
47
+ console.log(` ${c.gry}${ts()}${c.r} ${dot} ${c.b}iOS${c.r} ${c.d}${info}${c.r}`);
48
+ },
49
+
50
+ ui(action, count) {
51
+ const dot = action === 'connect' ? `${c.grn}●${c.r}` : `${c.red}○${c.r}`;
52
+ console.log(` ${c.gry}${ts()}${c.r} ${dot} ${c.b}UI${c.r} ${c.d}clients: ${count}${c.r}`);
53
+ },
54
+
55
+ register(debuggerId, name) {
56
+ console.log(` ${c.gry}${ts()}${c.r} ${c.grn}+${c.r} ${c.b}${name}${c.r} ${c.d}${debuggerId.slice(0, 12)}${c.r}`);
57
+ },
58
+
59
+ event(level, debuggerId, eventName, contextId) {
60
+ const s = LEVEL[level] ?? LEVEL.info;
61
+ const id = debuggerId.slice(0, 8);
62
+ const ctx = contextId ? `${c.d}${contextId}${c.r} ` : '';
63
+ console.log(` ${c.gry}${ts()}${c.r} ${s.color}${s.tag}${c.r} ${c.gry}${id}${c.r} ${ctx}${eventName}`);
64
+ },
65
+
66
+ perfpoint(debuggerId, badge, label, duration) {
67
+ const id = debuggerId.slice(0, 8);
68
+ const dur = duration < 1000 ? `${Math.round(duration)}ms` : `${(duration / 1000).toFixed(1)}s`;
69
+ const dc = duration < 300 ? c.grn : duration < 1000 ? c.ylw : c.red;
70
+ console.log(` ${c.gry}${ts()}${c.r} ${c.mag}PP${c.r} ${c.gry}${id}${c.r} ${badge} ${c.d}${label}${c.r} ${dc}${dur}${c.r}`);
71
+ },
72
+ };