@fias/plugin-dev-harness 1.1.5 → 1.1.7
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/dist/cli/submit.d.ts.map +1 -1
- package/dist/cli/submit.js +13 -6
- package/dist/cli/submit.js.map +1 -1
- package/dist/cli/submit.test.js +7 -6
- package/dist/cli/submit.test.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/mocks/theme.d.ts +6 -34
- package/dist/mocks/theme.d.ts.map +1 -1
- package/dist/mocks/theme.js +8 -46
- package/dist/mocks/theme.js.map +1 -1
- package/dist/server/harness-server.js +7 -7
- package/dist/server/static/harness.css +7 -7
- package/dist/server/static/harness.js +10 -10
- package/dist/server/static/static/harness.css +332 -0
- package/dist/server/static/static/harness.html +53 -0
- package/dist/server/static/static/harness.js +550 -0
- package/package.json +11 -11
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fias Arche Dev — Client-side Bridge Host
|
|
3
|
+
*
|
|
4
|
+
* Ported from the production PluginBridgeHost
|
|
5
|
+
* (client/src/arche/components/PluginBridge.ts).
|
|
6
|
+
*
|
|
7
|
+
* Handles postMessage communication with the plugin iframe,
|
|
8
|
+
* enforcing permissions and rate limits, and proxying server-side
|
|
9
|
+
* operations through the harness Express server.
|
|
10
|
+
*/
|
|
11
|
+
(function () {
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
var iframe = document.getElementById('plugin-iframe');
|
|
15
|
+
var consoleBody = document.getElementById('console-body');
|
|
16
|
+
var consoleCount = document.getElementById('console-count');
|
|
17
|
+
var consoleToggle = document.getElementById('console-toggle');
|
|
18
|
+
var creditBalance = document.getElementById('credit-balance');
|
|
19
|
+
var themeToggle = document.getElementById('theme-toggle');
|
|
20
|
+
var reloadBtn = document.getElementById('reload-btn');
|
|
21
|
+
var modeBadge = document.getElementById('mode-badge');
|
|
22
|
+
var pluginStatus = document.getElementById('plugin-status');
|
|
23
|
+
var themeBadge = document.getElementById('theme-badge');
|
|
24
|
+
var loginModal = document.getElementById('login-modal');
|
|
25
|
+
var loginInput = document.getElementById('login-input');
|
|
26
|
+
var loginError = document.getElementById('login-error');
|
|
27
|
+
var loginSubmit = document.getElementById('login-submit');
|
|
28
|
+
var loginCancel = document.getElementById('login-cancel');
|
|
29
|
+
|
|
30
|
+
var messageCount = 0;
|
|
31
|
+
var currentTheme = 'dark';
|
|
32
|
+
var currentMode = 'mock';
|
|
33
|
+
var hasCredentials = false;
|
|
34
|
+
var cachedConfig = null;
|
|
35
|
+
|
|
36
|
+
/** Permission requirements per bridge call type (matches production) */
|
|
37
|
+
var PERMISSION_MAP = {
|
|
38
|
+
get_user: 'user:profile:read',
|
|
39
|
+
get_theme: 'theme:read',
|
|
40
|
+
entity_invoke: 'entities:invoke',
|
|
41
|
+
storage_read: 'storage:sandbox',
|
|
42
|
+
storage_write: 'storage:sandbox',
|
|
43
|
+
storage_list: 'storage:sandbox',
|
|
44
|
+
storage_delete: 'storage:sandbox',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Rate limits per message type (matches production) */
|
|
48
|
+
var RATE_LIMITS = {
|
|
49
|
+
entity_invoke: { maxPerMinute: 60 },
|
|
50
|
+
storage_write: { maxPerMinute: 120 },
|
|
51
|
+
storage_read: { maxPerMinute: 300 },
|
|
52
|
+
storage_list: { maxPerMinute: 60 },
|
|
53
|
+
storage_delete: { maxPerMinute: 60 },
|
|
54
|
+
};
|
|
55
|
+
var rateBuckets = {};
|
|
56
|
+
|
|
57
|
+
// ────────────────────────────────────────────────────────────────
|
|
58
|
+
// Initialization
|
|
59
|
+
// ────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
fetchConfig().then(function (config) {
|
|
62
|
+
cachedConfig = config;
|
|
63
|
+
currentTheme = config.mockTheme || 'dark';
|
|
64
|
+
currentMode = config.mode || 'mock';
|
|
65
|
+
hasCredentials = config.hasCredentials || false;
|
|
66
|
+
|
|
67
|
+
updateThemeBadge();
|
|
68
|
+
updateModeBadge();
|
|
69
|
+
|
|
70
|
+
if (currentMode === 'live') {
|
|
71
|
+
creditBalance.style.display = 'inline';
|
|
72
|
+
fetchCredits();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if plugin server is reachable, then load
|
|
76
|
+
checkPluginReachable(config.pluginUrl, function (reachable) {
|
|
77
|
+
if (reachable) {
|
|
78
|
+
pluginStatus.classList.add('hidden');
|
|
79
|
+
iframe.classList.remove('hidden');
|
|
80
|
+
iframe.src = config.pluginUrl;
|
|
81
|
+
} else {
|
|
82
|
+
showPluginError(config.pluginUrl);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ────────────────────────────────────────────────────────────────
|
|
88
|
+
// UI Controls
|
|
89
|
+
// ────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
consoleToggle.addEventListener('click', function () {
|
|
92
|
+
consoleBody.classList.toggle('open');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
reloadBtn.addEventListener('click', function () {
|
|
96
|
+
if (cachedConfig) {
|
|
97
|
+
// Re-check reachability on reload
|
|
98
|
+
pluginStatus.classList.remove('hidden', 'error');
|
|
99
|
+
pluginStatus.innerHTML =
|
|
100
|
+
'<div class="status-spinner"></div><p>Connecting to plugin server...</p>';
|
|
101
|
+
iframe.classList.add('hidden');
|
|
102
|
+
checkPluginReachable(cachedConfig.pluginUrl, function (reachable) {
|
|
103
|
+
if (reachable) {
|
|
104
|
+
pluginStatus.classList.add('hidden');
|
|
105
|
+
iframe.classList.remove('hidden');
|
|
106
|
+
iframe.src = cachedConfig.pluginUrl;
|
|
107
|
+
} else {
|
|
108
|
+
showPluginError(cachedConfig.pluginUrl);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
iframe.src = iframe.src;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
themeToggle.addEventListener('click', function () {
|
|
117
|
+
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
118
|
+
updateThemeBadge();
|
|
119
|
+
sendToPlugin({
|
|
120
|
+
type: 'theme_update',
|
|
121
|
+
messageId: 'theme_' + Date.now(),
|
|
122
|
+
payload: getTheme(),
|
|
123
|
+
});
|
|
124
|
+
logMessage('send', 'theme_update', { mode: currentTheme });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Mode toggle — click the badge to switch between mock/live
|
|
128
|
+
modeBadge.addEventListener('click', function () {
|
|
129
|
+
if (currentMode === 'mock') {
|
|
130
|
+
// Switching to live — need credentials
|
|
131
|
+
if (!hasCredentials) {
|
|
132
|
+
showLoginModal();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
switchMode('live');
|
|
136
|
+
} else {
|
|
137
|
+
switchMode('mock');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
function switchMode(newMode) {
|
|
142
|
+
fetch('/api/mode', {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
headers: { 'Content-Type': 'application/json' },
|
|
145
|
+
body: JSON.stringify({ mode: newMode }),
|
|
146
|
+
})
|
|
147
|
+
.then(function (r) {
|
|
148
|
+
if (!r.ok) {
|
|
149
|
+
return r.json().then(function (err) {
|
|
150
|
+
throw new Error(err.error || 'Failed to switch mode');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return r.json();
|
|
154
|
+
})
|
|
155
|
+
.then(function (data) {
|
|
156
|
+
currentMode = data.mode;
|
|
157
|
+
updateModeBadge();
|
|
158
|
+
logMessage('info', 'Mode switched to ' + currentMode.toUpperCase());
|
|
159
|
+
|
|
160
|
+
if (currentMode === 'live') {
|
|
161
|
+
creditBalance.style.display = 'inline';
|
|
162
|
+
fetchCredits();
|
|
163
|
+
} else {
|
|
164
|
+
creditBalance.style.display = 'none';
|
|
165
|
+
creditBalance.textContent = '';
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
.catch(function (err) {
|
|
169
|
+
logMessage('error', err.message);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ────────────────────────────────────────────────────────────────
|
|
174
|
+
// Login Modal
|
|
175
|
+
// ────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
function showLoginModal() {
|
|
178
|
+
loginModal.style.display = 'flex';
|
|
179
|
+
loginInput.value = '';
|
|
180
|
+
loginError.style.display = 'none';
|
|
181
|
+
loginInput.focus();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function hideLoginModal() {
|
|
185
|
+
loginModal.style.display = 'none';
|
|
186
|
+
loginInput.value = '';
|
|
187
|
+
loginError.style.display = 'none';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
loginCancel.addEventListener('click', hideLoginModal);
|
|
191
|
+
|
|
192
|
+
loginModal.addEventListener('click', function (e) {
|
|
193
|
+
if (e.target === loginModal) hideLoginModal();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
loginInput.addEventListener('keydown', function (e) {
|
|
197
|
+
if (e.key === 'Enter') submitLogin();
|
|
198
|
+
if (e.key === 'Escape') hideLoginModal();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
loginSubmit.addEventListener('click', submitLogin);
|
|
202
|
+
|
|
203
|
+
function submitLogin() {
|
|
204
|
+
var apiKey = loginInput.value.trim();
|
|
205
|
+
if (!apiKey) {
|
|
206
|
+
loginError.textContent = 'Please enter an API key.';
|
|
207
|
+
loginError.style.display = 'block';
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
loginSubmit.disabled = true;
|
|
212
|
+
loginSubmit.textContent = 'Connecting...';
|
|
213
|
+
|
|
214
|
+
fetch('/api/login', {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: { 'Content-Type': 'application/json' },
|
|
217
|
+
body: JSON.stringify({ apiKey: apiKey }),
|
|
218
|
+
})
|
|
219
|
+
.then(function (r) {
|
|
220
|
+
if (!r.ok) {
|
|
221
|
+
return r.json().then(function (err) {
|
|
222
|
+
throw new Error(err.error || 'Login failed');
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return r.json();
|
|
226
|
+
})
|
|
227
|
+
.then(function () {
|
|
228
|
+
hasCredentials = true;
|
|
229
|
+
hideLoginModal();
|
|
230
|
+
logMessage('info', 'API key saved successfully');
|
|
231
|
+
// Automatically switch to live mode after login
|
|
232
|
+
switchMode('live');
|
|
233
|
+
})
|
|
234
|
+
.catch(function (err) {
|
|
235
|
+
loginError.textContent = err.message;
|
|
236
|
+
loginError.style.display = 'block';
|
|
237
|
+
})
|
|
238
|
+
.finally(function () {
|
|
239
|
+
loginSubmit.disabled = false;
|
|
240
|
+
loginSubmit.textContent = 'Save & Connect';
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ────────────────────────────────────────────────────────────────
|
|
245
|
+
// Message Handling
|
|
246
|
+
// ────────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
window.addEventListener('message', function (event) {
|
|
249
|
+
if (event.source !== iframe.contentWindow) return;
|
|
250
|
+
|
|
251
|
+
var data = event.data;
|
|
252
|
+
if (!data || typeof data !== 'object' || !data.type || !data.messageId) return;
|
|
253
|
+
|
|
254
|
+
logMessage('recv', data.type, data.payload);
|
|
255
|
+
|
|
256
|
+
// Fire-and-forget messages
|
|
257
|
+
if (data.type === 'ready') return;
|
|
258
|
+
|
|
259
|
+
if (data.type === 'resize') {
|
|
260
|
+
var height = data.payload && data.payload.height;
|
|
261
|
+
if (typeof height === 'number' && height > 0) {
|
|
262
|
+
iframe.style.height = height + 'px';
|
|
263
|
+
iframe.style.flex = 'none';
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (data.type === 'toast') {
|
|
269
|
+
var msg = data.payload && data.payload.message;
|
|
270
|
+
if (typeof msg === 'string') {
|
|
271
|
+
logMessage('toast', msg, data.payload);
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (data.type === 'navigate') {
|
|
277
|
+
var navPath = data.payload && data.payload.path;
|
|
278
|
+
if (typeof navPath === 'string') {
|
|
279
|
+
logMessage('nav', navPath);
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Request/response messages — enforce permissions + rate limits, then proxy
|
|
285
|
+
handleRequest(data);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
function handleRequest(data) {
|
|
289
|
+
try {
|
|
290
|
+
// Check permissions
|
|
291
|
+
var requiredPerm = PERMISSION_MAP[data.type];
|
|
292
|
+
if (requiredPerm && cachedConfig && cachedConfig.permissions.indexOf(requiredPerm) === -1) {
|
|
293
|
+
throw new Error('Permission denied: ' + requiredPerm + ' not granted');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check rate limit
|
|
297
|
+
checkRateLimit(data.type);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
logMessage('error', err.message);
|
|
300
|
+
sendToPlugin({
|
|
301
|
+
type: 'response',
|
|
302
|
+
messageId: data.messageId,
|
|
303
|
+
payload: null,
|
|
304
|
+
error: err.message,
|
|
305
|
+
});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Proxy to harness server
|
|
310
|
+
fetch('/api/bridge', {
|
|
311
|
+
method: 'POST',
|
|
312
|
+
headers: { 'Content-Type': 'application/json' },
|
|
313
|
+
body: JSON.stringify({ type: data.type, payload: data.payload }),
|
|
314
|
+
})
|
|
315
|
+
.then(function (response) {
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
return response.json().then(function (err) {
|
|
318
|
+
throw new Error(err.error || 'Bridge call failed');
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return response.json();
|
|
322
|
+
})
|
|
323
|
+
.then(function (result) {
|
|
324
|
+
logMessage('send', 'response', result);
|
|
325
|
+
|
|
326
|
+
// Show cost for entity invocations in live mode
|
|
327
|
+
if (
|
|
328
|
+
data.type === 'entity_invoke' &&
|
|
329
|
+
result.metadata &&
|
|
330
|
+
result.metadata.cost > 0
|
|
331
|
+
) {
|
|
332
|
+
logMessage('cost', 'Credits used: ' + result.metadata.cost.toFixed(4));
|
|
333
|
+
fetchCredits();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
sendToPlugin({
|
|
337
|
+
type: 'response',
|
|
338
|
+
messageId: data.messageId,
|
|
339
|
+
payload: result,
|
|
340
|
+
});
|
|
341
|
+
})
|
|
342
|
+
.catch(function (err) {
|
|
343
|
+
logMessage('error', err.message);
|
|
344
|
+
sendToPlugin({
|
|
345
|
+
type: 'response',
|
|
346
|
+
messageId: data.messageId,
|
|
347
|
+
payload: null,
|
|
348
|
+
error: err.message,
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ────────────────────────────────────────────────────────────────
|
|
354
|
+
// Iframe Init
|
|
355
|
+
// ────────────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
iframe.addEventListener('load', function () {
|
|
358
|
+
if (!cachedConfig) return;
|
|
359
|
+
|
|
360
|
+
sendToPlugin({
|
|
361
|
+
type: 'init',
|
|
362
|
+
messageId: 'init_0',
|
|
363
|
+
payload: {
|
|
364
|
+
archId: 'dev_harness',
|
|
365
|
+
permissions: cachedConfig.permissions,
|
|
366
|
+
theme: getTheme(),
|
|
367
|
+
currentPath: '/',
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
logMessage('send', 'init');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// ────────────────────────────────────────────────────────────────
|
|
374
|
+
// Helpers
|
|
375
|
+
// ────────────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
function updateModeBadge() {
|
|
378
|
+
var opposite = currentMode === 'live' ? 'Mock' : 'Live';
|
|
379
|
+
modeBadge.textContent = currentMode.toUpperCase() + ' \u21C6';
|
|
380
|
+
modeBadge.className = 'mode-badge ' + (currentMode === 'live' ? 'mode-live' : 'mode-mock');
|
|
381
|
+
modeBadge.title = 'Click to switch to ' + opposite + ' mode';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function updateThemeBadge() {
|
|
385
|
+
themeBadge.textContent = currentTheme.toUpperCase();
|
|
386
|
+
themeBadge.className = 'theme-badge theme-' + currentTheme;
|
|
387
|
+
document.body.style.background = currentTheme === 'light' ? '#ffffff' : '#0a0a0a';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function sendToPlugin(message) {
|
|
391
|
+
// targetOrigin '*' because the plugin may be on a different port.
|
|
392
|
+
// Security is enforced by checking event.source on incoming messages.
|
|
393
|
+
iframe.contentWindow && iframe.contentWindow.postMessage(message, '*');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function checkRateLimit(type) {
|
|
397
|
+
var limit = RATE_LIMITS[type];
|
|
398
|
+
if (!limit) return;
|
|
399
|
+
|
|
400
|
+
var now = Date.now();
|
|
401
|
+
var bucket = rateBuckets[type];
|
|
402
|
+
|
|
403
|
+
if (!bucket || now - bucket.windowStart > 60000) {
|
|
404
|
+
rateBuckets[type] = { count: 1, windowStart: now };
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (bucket.count >= limit.maxPerMinute) {
|
|
409
|
+
throw new Error(
|
|
410
|
+
'Rate limit exceeded for ' + type + ': max ' + limit.maxPerMinute + '/minute',
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
bucket.count++;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
var SYSTEM_FONTS = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
418
|
+
var MONO_FONTS = '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace';
|
|
419
|
+
var SPACING = { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' };
|
|
420
|
+
var FONTS = { body: SYSTEM_FONTS, heading: SYSTEM_FONTS, mono: MONO_FONTS };
|
|
421
|
+
|
|
422
|
+
function getTheme() {
|
|
423
|
+
if (currentTheme === 'light') {
|
|
424
|
+
return {
|
|
425
|
+
mode: 'light',
|
|
426
|
+
colors: {
|
|
427
|
+
primary: '#171717', secondary: '#e5e5e5',
|
|
428
|
+
background: '#ffffff', surface: '#fafafa',
|
|
429
|
+
text: '#0a0a0a', textSecondary: '#737373',
|
|
430
|
+
border: '#e5e5e5',
|
|
431
|
+
error: '#dc2626', warning: '#d97706', success: '#16a34a',
|
|
432
|
+
},
|
|
433
|
+
spacing: SPACING,
|
|
434
|
+
fonts: FONTS,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
mode: 'dark',
|
|
439
|
+
colors: {
|
|
440
|
+
primary: '#ffffff', secondary: '#1f1f1f',
|
|
441
|
+
background: '#0a0a0a', surface: '#171717',
|
|
442
|
+
text: '#ffffff', textSecondary: '#a6a6a6',
|
|
443
|
+
border: '#2e2e2e',
|
|
444
|
+
error: '#ef4444', warning: '#f59e0b', success: '#22c55e',
|
|
445
|
+
},
|
|
446
|
+
spacing: SPACING,
|
|
447
|
+
fonts: FONTS,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function fetchConfig() {
|
|
452
|
+
return fetch('/api/config').then(function (r) {
|
|
453
|
+
return r.json();
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function fetchCredits() {
|
|
458
|
+
fetch('/api/credits')
|
|
459
|
+
.then(function (r) {
|
|
460
|
+
return r.json();
|
|
461
|
+
})
|
|
462
|
+
.then(function (data) {
|
|
463
|
+
if (data.balance !== undefined && data.balance !== null && isFinite(data.balance)) {
|
|
464
|
+
creditBalance.textContent = 'Credits: ' + data.balance.toFixed(2);
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
.catch(function () {});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function logMessage(direction, type, payload) {
|
|
471
|
+
messageCount++;
|
|
472
|
+
consoleCount.textContent = messageCount + ' messages';
|
|
473
|
+
|
|
474
|
+
var entry = document.createElement('div');
|
|
475
|
+
entry.className = 'log-entry';
|
|
476
|
+
|
|
477
|
+
var time = new Date().toLocaleTimeString();
|
|
478
|
+
var cls = 'log-info';
|
|
479
|
+
if (direction === 'error') cls = 'log-error';
|
|
480
|
+
if (direction === 'warn' || direction === 'cost') cls = 'log-warn';
|
|
481
|
+
if (direction === 'toast') cls = 'log-warn';
|
|
482
|
+
|
|
483
|
+
var text = '<span class="log-time">' + escapeHtml(time) + '</span>';
|
|
484
|
+
text +=
|
|
485
|
+
'<span class="' +
|
|
486
|
+
cls +
|
|
487
|
+
'">[' +
|
|
488
|
+
escapeHtml(direction.toUpperCase()) +
|
|
489
|
+
'] ' +
|
|
490
|
+
escapeHtml(String(type)) +
|
|
491
|
+
'</span>';
|
|
492
|
+
if (payload && typeof payload === 'object') {
|
|
493
|
+
text +=
|
|
494
|
+
' <span style="color:#6b7280">' +
|
|
495
|
+
escapeHtml(JSON.stringify(payload).substring(0, 120)) +
|
|
496
|
+
'</span>';
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
entry.innerHTML = text;
|
|
500
|
+
consoleBody.appendChild(entry);
|
|
501
|
+
consoleBody.scrollTop = consoleBody.scrollHeight;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function escapeHtml(str) {
|
|
505
|
+
var div = document.createElement('div');
|
|
506
|
+
div.appendChild(document.createTextNode(str));
|
|
507
|
+
return div.innerHTML;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function checkPluginReachable(url, callback) {
|
|
511
|
+
var done = false;
|
|
512
|
+
|
|
513
|
+
// Try fetching through the harness server to avoid CORS issues
|
|
514
|
+
fetch('/api/check-plugin')
|
|
515
|
+
.then(function (r) { return r.json(); })
|
|
516
|
+
.then(function (data) {
|
|
517
|
+
if (!done) {
|
|
518
|
+
done = true;
|
|
519
|
+
callback(data.reachable);
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
.catch(function () {
|
|
523
|
+
if (!done) {
|
|
524
|
+
done = true;
|
|
525
|
+
// If the check endpoint doesn't exist, assume reachable and let iframe handle it
|
|
526
|
+
callback(true);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// Timeout after 5 seconds
|
|
531
|
+
setTimeout(function () {
|
|
532
|
+
if (!done) {
|
|
533
|
+
done = true;
|
|
534
|
+
callback(false);
|
|
535
|
+
}
|
|
536
|
+
}, 5000);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function showPluginError(url) {
|
|
540
|
+
pluginStatus.classList.add('error');
|
|
541
|
+
pluginStatus.innerHTML =
|
|
542
|
+
'<p><strong>Plugin server not reachable</strong></p>' +
|
|
543
|
+
'<p>The harness cannot connect to your plugin at<br/><code>' + escapeHtml(url) + '</code></p>' +
|
|
544
|
+
'<p>Make sure your plugin dev server is running:</p>' +
|
|
545
|
+
'<p><code>npm run dev</code></p>' +
|
|
546
|
+
'<p style="color:#6b7280;font-size:12px;margin-top:8px;">' +
|
|
547
|
+
'The harness will automatically retry when you click the reload button (↻).</p>';
|
|
548
|
+
iframe.classList.add('hidden');
|
|
549
|
+
}
|
|
550
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fias/plugin-dev-harness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "Development harness for building and testing FIAS plugin arches locally",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -15,12 +15,6 @@
|
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "tsc && cp -r src/server/static dist/server/static",
|
|
20
|
-
"clean": "rm -rf dist",
|
|
21
|
-
"watch": "tsc -w",
|
|
22
|
-
"typecheck": "tsc --noEmit"
|
|
23
|
-
},
|
|
24
18
|
"keywords": [
|
|
25
19
|
"fias",
|
|
26
20
|
"plugin",
|
|
@@ -30,14 +24,20 @@
|
|
|
30
24
|
],
|
|
31
25
|
"license": "MIT",
|
|
32
26
|
"dependencies": {
|
|
27
|
+
"chalk": "^4.1.2",
|
|
33
28
|
"commander": "^12.0.0",
|
|
34
29
|
"express": "^4.21.0",
|
|
35
|
-
"
|
|
36
|
-
"chalk": "^4.1.2"
|
|
30
|
+
"@fias/arche-sdk": "1.1.5"
|
|
37
31
|
},
|
|
38
32
|
"devDependencies": {
|
|
39
33
|
"@types/express": "^5.0.0",
|
|
40
|
-
"@types/node": "^25.
|
|
34
|
+
"@types/node": "^25.5.0",
|
|
41
35
|
"typescript": "^5.3.3"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc && cp -r src/server/static dist/server/static",
|
|
39
|
+
"clean": "rm -rf dist",
|
|
40
|
+
"watch": "tsc -w",
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|