skopix 2.0.106 → 2.0.108
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/cli/commands/agent.js +169 -22
- package/package.json +1 -1
package/cli/commands/agent.js
CHANGED
|
@@ -387,18 +387,18 @@ export async function agentCommand(options) {
|
|
|
387
387
|
const startUrl = (msg.startUrl && msg.startUrl.startsWith('http')) ? msg.startUrl : null;
|
|
388
388
|
console.log(chalk.cyan(' ⏸ Debug replay: ') + chalk.white(replaySteps.length + ' steps') + (startUrl ? chalk.dim(' @ ' + startUrl) : ''));
|
|
389
389
|
|
|
390
|
-
|
|
390
|
+
// IMPORTANT: dashboard listens for 'jobUpdate' (replay) and 'recordingUpdate' (recording)
|
|
391
|
+
const sendRun = (data) => { try { ws.send(JSON.stringify({ type: 'jobUpdate', runId, data })); } catch {} };
|
|
391
392
|
const sendRec = (data) => { try { ws.send(JSON.stringify({ type: 'recordingUpdate', recordingId, data })); } catch {} };
|
|
392
393
|
const steps = [];
|
|
394
|
+
let chromiumBrowser = null;
|
|
395
|
+
let page = null;
|
|
396
|
+
let ctx = null;
|
|
393
397
|
|
|
394
398
|
sendRun({ type: 'stdout', text: '' });
|
|
395
399
|
sendRun({ type: 'stdout', text: ' Replaying ' + replaySteps.length + ' steps to reach debug point...' });
|
|
396
400
|
sendRun({ type: 'stdout', text: '\u2501'.repeat(60) });
|
|
397
401
|
|
|
398
|
-
let chromiumBrowser = null;
|
|
399
|
-
let page = null;
|
|
400
|
-
let ctx = null;
|
|
401
|
-
|
|
402
402
|
try {
|
|
403
403
|
const { chromium } = await import('playwright');
|
|
404
404
|
chromiumBrowser = await chromium.launch({ headless: false, args: ['--no-sandbox', '--disable-blink-features=AutomationControlled', '--allow-insecure-localhost'] });
|
|
@@ -410,7 +410,9 @@ export async function agentCommand(options) {
|
|
|
410
410
|
await page.waitForTimeout(1000);
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
|
|
413
|
+
sendRun({ type: 'stdout', text: ' Replaying ' + replaySteps.length + ' steps...' });
|
|
414
|
+
|
|
415
|
+
// Replay each step to the debug point (no toolbar yet — matches original solo flow)
|
|
414
416
|
let stepNum = 0;
|
|
415
417
|
let browserClosed = false;
|
|
416
418
|
for (const step of replaySteps) {
|
|
@@ -423,28 +425,28 @@ export async function agentCommand(options) {
|
|
|
423
425
|
await executeStep(step, sel, page, { url: startUrl || '' });
|
|
424
426
|
sendRun({ type: 'stdout', text: ' \u2713 Done' });
|
|
425
427
|
} catch (err) {
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
+
const emsg = err.message || '';
|
|
429
|
+
if (emsg.includes('closed') || emsg.includes('destroyed') || emsg.includes('detached')) {
|
|
428
430
|
browserClosed = true;
|
|
429
|
-
sendRun({ type: 'stdout', text: ' \u2716 Browser closed' });
|
|
431
|
+
sendRun({ type: 'stdout', text: ' \u2716 Browser closed \u2014 stopping replay early' });
|
|
430
432
|
} else {
|
|
431
|
-
sendRun({ type: 'stdout', text: ' \u26a0
|
|
433
|
+
sendRun({ type: 'stdout', text: ' \u26a0 Step ' + stepNum + ' skipped: ' + emsg.slice(0, 100) });
|
|
432
434
|
}
|
|
433
435
|
}
|
|
434
436
|
}
|
|
435
437
|
|
|
436
438
|
sendRun({ type: 'stdout', text: '' });
|
|
437
|
-
sendRun({ type: 'stdout', text: '
|
|
438
|
-
sendRun({ type: 'stdout', text: '
|
|
439
|
+
sendRun({ type: 'stdout', text: '\u2501'.repeat(60) });
|
|
440
|
+
sendRun({ type: 'stdout', text: ' \u2714 Reached step ' + replaySteps.length + ' \u2014 browser is ready' });
|
|
441
|
+
sendRun({ type: 'stdout', text: ' Now recording. Use the browser, then click Stop.' });
|
|
442
|
+
sendRun({ type: 'stdout', text: '\u2501'.repeat(60) });
|
|
439
443
|
|
|
440
|
-
// Wire up capture
|
|
444
|
+
// Wire up capture on context — persists across navigations
|
|
441
445
|
await ctx.exposeFunction('__skopixCapture', async (actionData) => {
|
|
442
446
|
if (actionData.action === 'stop') {
|
|
443
|
-
// Send done to close replay SSE, then send recording done
|
|
444
|
-
sendRun({ type: 'done', exitCode: 0, status: 'passed', recordingId });
|
|
445
|
-
// Save steps
|
|
446
|
-
const screenshotDir = path.join(os.homedir(), '.skopix', 'recordings', recordingId);
|
|
447
447
|
sendRec({ type: 'done', steps });
|
|
448
|
+
sendRec({ type: 'stopped' });
|
|
449
|
+
try { await ctx.close(); } catch {}
|
|
448
450
|
try { await chromiumBrowser.close(); } catch {}
|
|
449
451
|
ws.send(JSON.stringify({ type: 'jobDone', runId }));
|
|
450
452
|
console.log(chalk.green(' \u2714 Debug recording done \u2014 ' + steps.length + ' new steps'));
|
|
@@ -467,7 +469,6 @@ export async function agentCommand(options) {
|
|
|
467
469
|
sendRec({ type: 'step', step: recStep });
|
|
468
470
|
process.stdout.write(chalk.cyan(' \u23fa ') + (recStep.action || '') + '\n');
|
|
469
471
|
try { await page.evaluate(n => { if (window.__skopixUpdateCount) window.__skopixUpdateCount(n); }, steps.length); } catch {}
|
|
470
|
-
// Screenshot
|
|
471
472
|
setTimeout(async () => {
|
|
472
473
|
try {
|
|
473
474
|
const screenshotDir = path.join(os.homedir(), '.skopix', 'recordings', recordingId);
|
|
@@ -480,8 +481,154 @@ export async function agentCommand(options) {
|
|
|
480
481
|
}, 400);
|
|
481
482
|
});
|
|
482
483
|
|
|
483
|
-
//
|
|
484
|
-
|
|
484
|
+
// addInitScript so toolbar re-injects on any future navigation during recording
|
|
485
|
+
await ctx.addInitScript(() => {
|
|
486
|
+
if (window.__skopixRecording) return;
|
|
487
|
+
window.__skopixRecording = true;
|
|
488
|
+
|
|
489
|
+
function getSelector(el) {
|
|
490
|
+
if (!el || el === document.body) return 'body';
|
|
491
|
+
const testAttrs = ['data-testid','data-test','pi-test-identifier','data-cy','data-qa'];
|
|
492
|
+
for (const attr of testAttrs) { const val = el.getAttribute(attr); if (val) return '['+attr+'="'+val+'"]'; }
|
|
493
|
+
if (el.id && !/^\d/.test(el.id)) return '#'+el.id;
|
|
494
|
+
const al = el.getAttribute('aria-label');
|
|
495
|
+
if (al && ['button','a','input'].includes(el.tagName.toLowerCase())) return el.tagName.toLowerCase()+'[aria-label="'+al+'"]';
|
|
496
|
+
if (el.name && el.tagName === 'INPUT') return 'input[name="'+el.name+'"]';
|
|
497
|
+
const parts = []; let cur = el, depth = 0;
|
|
498
|
+
while (cur && cur !== document.body && depth < 4) {
|
|
499
|
+
let seg = cur.tagName.toLowerCase();
|
|
500
|
+
if (cur.id && !/^\d/.test(cur.id)) { parts.unshift('#'+cur.id); break; }
|
|
501
|
+
const sib = Array.from(cur.parentElement ? cur.parentElement.children : []).filter(c => c.tagName === cur.tagName);
|
|
502
|
+
if (sib.length > 1) seg += ':nth-of-type('+(sib.indexOf(cur)+1)+')';
|
|
503
|
+
parts.unshift(seg); cur = cur.parentElement; depth++;
|
|
504
|
+
}
|
|
505
|
+
return parts.join(' > ');
|
|
506
|
+
}
|
|
507
|
+
function getElementInfo(el) {
|
|
508
|
+
return { tag: el.tagName.toLowerCase(), id: el.id||null, name: el.name||null, type: el.type||null,
|
|
509
|
+
text: (el.innerText||el.value||el.placeholder||el.getAttribute('aria-label')||'').trim().slice(0,80),
|
|
510
|
+
selector: getSelector(el), classes: el.className ? el.className.toString().trim().slice(0,100) : null,
|
|
511
|
+
piTestId: el.getAttribute('pi-test-identifier')||null, title: el.getAttribute('title')||null,
|
|
512
|
+
ariaLabel: el.getAttribute('aria-label')||null, dataTestId: el.getAttribute('data-testid')||null };
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
document.addEventListener('click', function(e) {
|
|
516
|
+
if (e.target && e.target.closest) { if (e.target.closest('#__skopix_toolbar')) return; if (e.target.closest('#__skopix_popover')) return; if (e.target.closest('#__skopix_hint')) return; }
|
|
517
|
+
if (window.__skopixPickMode) return;
|
|
518
|
+
const el = e.target;
|
|
519
|
+
if (!el || el === document.body || el === document.documentElement) return;
|
|
520
|
+
const rect = el.getBoundingClientRect();
|
|
521
|
+
const isCheckable = el.type === 'checkbox' || el.type === 'radio';
|
|
522
|
+
let checkTarget = null;
|
|
523
|
+
if (!isCheckable && el.tagName === 'LABEL' && el.htmlFor) checkTarget = document.getElementById(el.htmlFor);
|
|
524
|
+
if (!isCheckable && el.tagName === 'LABEL' && !el.htmlFor) checkTarget = el.querySelector('input[type="checkbox"],input[type="radio"]');
|
|
525
|
+
const actual = isCheckable ? el : checkTarget;
|
|
526
|
+
if (actual && (actual.type === 'checkbox' || actual.type === 'radio')) {
|
|
527
|
+
window.__skopixCapture && window.__skopixCapture({ action:'check', checked:actual.checked, element:getElementInfo(actual), clickX:Math.round(e.clientX), clickY:Math.round(e.clientY), elementX:Math.round(rect.left+rect.width/2), elementY:Math.round(rect.top+rect.height/2) });
|
|
528
|
+
} else {
|
|
529
|
+
window.__skopixCapture && window.__skopixCapture({ action:'click', element:getElementInfo(el), clickX:Math.round(e.clientX), clickY:Math.round(e.clientY), elementX:Math.round(rect.left+rect.width/2), elementY:Math.round(rect.top+rect.height/2) });
|
|
530
|
+
}
|
|
531
|
+
}, true);
|
|
532
|
+
|
|
533
|
+
let typeTimer = null, lastInputEl = null;
|
|
534
|
+
document.addEventListener('input', function(e) {
|
|
535
|
+
const el = e.target;
|
|
536
|
+
if (!el || !['INPUT','TEXTAREA'].includes(el.tagName)) return;
|
|
537
|
+
if (el.type === 'checkbox' || el.type === 'radio') return;
|
|
538
|
+
if (el.closest && (el.closest('#__skopix_toolbar') || el.closest('#__skopix_popover'))) return;
|
|
539
|
+
lastInputEl = el; clearTimeout(typeTimer);
|
|
540
|
+
typeTimer = setTimeout(() => { if (!lastInputEl) return; window.__skopixCapture && window.__skopixCapture({ action:'type', element:getElementInfo(lastInputEl), value:lastInputEl.value, isPassword:lastInputEl.type==='password' }); lastInputEl = null; }, 600);
|
|
541
|
+
}, true);
|
|
542
|
+
|
|
543
|
+
document.addEventListener('change', function(e) {
|
|
544
|
+
const el = e.target; if (!el || el.tagName !== 'SELECT') return;
|
|
545
|
+
if (el.closest && (el.closest('#__skopix_toolbar') || el.closest('#__skopix_popover'))) return;
|
|
546
|
+
const sel2 = el.options[el.selectedIndex];
|
|
547
|
+
window.__skopixCapture && window.__skopixCapture({ action:'select', element:getElementInfo(el), value:el.value, label:sel2?sel2.text:el.value });
|
|
548
|
+
}, true);
|
|
549
|
+
|
|
550
|
+
// Toolbar
|
|
551
|
+
const tb = document.createElement('div');
|
|
552
|
+
tb.id = '__skopix_toolbar';
|
|
553
|
+
tb.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:2147483647;background:#0f1117;border:2px solid #f59e0b;border-radius:10px;padding:10px 14px;display:flex;align-items:center;gap:10px;font-family:monospace;font-size:12px;color:#e5e7eb;box-shadow:0 4px 24px rgba(0,0,0,0.6);user-select:none';
|
|
554
|
+
tb.innerHTML = '<span style="color:#f59e0b;font-size:14px;animation:skopix_pulse 1s infinite">\u25cf</span>'
|
|
555
|
+
+ '<span style="color:#9ca3af">Debug recording</span>'
|
|
556
|
+
+ '<span id="__skopix_count" style="color:#22d3ee;font-weight:700;min-width:20px;text-align:center">0</span>'
|
|
557
|
+
+ '<span style="color:#4b5563">steps</span>'
|
|
558
|
+
+ '<button id="__skopix_assert_btn" style="background:#1e3a5f;border:1px solid #2563eb;color:#60a5fa;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:11px;font-family:monospace">+ Assert</button>'
|
|
559
|
+
+ '<button id="__skopix_stop_btn" style="background:#3f0d0d;border:1px solid #dc2626;color:#f87171;border-radius:6px;padding:4px 10px;cursor:pointer;font-size:11px;font-family:monospace">\u25a0 Stop</button>';
|
|
560
|
+
const sty = document.createElement('style');
|
|
561
|
+
sty.textContent = '@keyframes skopix_pulse{0%,100%{opacity:1}50%{opacity:0.3}}';
|
|
562
|
+
document.head.appendChild(sty);
|
|
563
|
+
document.body.appendChild(tb);
|
|
564
|
+
tb.querySelector('#__skopix_stop_btn').addEventListener('click', e => { e.stopPropagation(); window.__skopixCapture && window.__skopixCapture({ action:'stop' }); });
|
|
565
|
+
window.__skopixUpdateCount = n => { const el = document.getElementById('__skopix_count'); if (el) el.textContent = n; };
|
|
566
|
+
|
|
567
|
+
// Assert picker
|
|
568
|
+
tb.querySelector('#__skopix_assert_btn').addEventListener('click', function(e) {
|
|
569
|
+
e.stopPropagation();
|
|
570
|
+
window.__skopixPickMode = true;
|
|
571
|
+
document.body.style.cursor = 'crosshair';
|
|
572
|
+
const hint = document.createElement('div'); hint.id = '__skopix_hint';
|
|
573
|
+
hint.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#1e3a5f;border:1px solid #2563eb;color:#60a5fa;padding:8px 18px;border-radius:8px;font-family:monospace;font-size:13px;pointer-events:none';
|
|
574
|
+
hint.textContent = 'Click any element to add an assertion \u2014 Esc to cancel';
|
|
575
|
+
document.body.appendChild(hint);
|
|
576
|
+
const overlay = document.createElement('div');
|
|
577
|
+
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:auto;cursor:crosshair';
|
|
578
|
+
document.body.appendChild(overlay);
|
|
579
|
+
const hl = document.createElement('div');
|
|
580
|
+
hl.style.cssText = 'position:fixed;pointer-events:none;z-index:2147483645;background:rgba(37,99,235,0.15);border:2px solid #2563eb;border-radius:3px;transition:all 0.1s;display:none';
|
|
581
|
+
document.body.appendChild(hl);
|
|
582
|
+
overlay.addEventListener('mousemove', function(e2) {
|
|
583
|
+
overlay.style.pointerEvents = 'none';
|
|
584
|
+
const el2 = document.elementFromPoint(e2.clientX, e2.clientY);
|
|
585
|
+
overlay.style.pointerEvents = 'auto';
|
|
586
|
+
if (!el2 || el2 === document.body) { hl.style.display = 'none'; return; }
|
|
587
|
+
const r = el2.getBoundingClientRect();
|
|
588
|
+
hl.style.display = 'block'; hl.style.top = r.top+'px'; hl.style.left = r.left+'px'; hl.style.width = r.width+'px'; hl.style.height = r.height+'px';
|
|
589
|
+
});
|
|
590
|
+
overlay.addEventListener('click', function(e2) {
|
|
591
|
+
e2.preventDefault(); e2.stopPropagation();
|
|
592
|
+
overlay.style.pointerEvents = 'none';
|
|
593
|
+
const el2 = document.elementFromPoint(e2.clientX, e2.clientY);
|
|
594
|
+
overlay.style.pointerEvents = 'auto';
|
|
595
|
+
window.__skopixPickMode = false; document.body.style.cursor = '';
|
|
596
|
+
overlay.remove(); const h2 = document.getElementById('__skopix_hint'); if (h2) h2.remove();
|
|
597
|
+
if (!el2 || el2 === document.body) { hl.style.display = 'none'; return; }
|
|
598
|
+
const sel2 = getSelector(el2);
|
|
599
|
+
const txt = (el2.innerText||el2.textContent||'').trim().slice(0,100);
|
|
600
|
+
const rect2 = el2.getBoundingClientRect();
|
|
601
|
+
hl.style.background = 'rgba(34,197,94,0.15)'; hl.style.borderColor = '#22c55e';
|
|
602
|
+
hl.style.display = 'block'; hl.style.top = rect2.top+'px'; hl.style.left = rect2.left+'px'; hl.style.width = rect2.width+'px'; hl.style.height = rect2.height+'px';
|
|
603
|
+
let sugType = 'visible', sugVal = '';
|
|
604
|
+
if (txt && txt.length > 0 && txt.length < 80) { sugType = 'text_contains'; sugVal = txt.replace(/\s+/g,' ').trim(); }
|
|
605
|
+
const popover = document.createElement('div'); popover.id = '__skopix_popover';
|
|
606
|
+
const topPos = rect2.bottom+8+280 > window.innerHeight ? Math.max(8,rect2.top-288) : rect2.bottom+8;
|
|
607
|
+
popover.style.cssText = 'position:fixed;z-index:2147483647;top:'+topPos+'px;left:'+Math.min(rect2.left,window.innerWidth-360)+'px;width:350px;background:#0f1117;border:1px solid #2563eb;border-radius:10px;padding:16px;font-family:monospace;font-size:12px;color:#e5e7eb;box-shadow:0 8px 32px rgba(0,0,0,0.7)';
|
|
608
|
+
popover.innerHTML = '<div style="color:#60a5fa;font-size:11px;letter-spacing:0.1em;margin-bottom:12px">ADD ASSERTION</div>'
|
|
609
|
+
+ '<div style="margin-bottom:10px"><div style="color:#9ca3af;font-size:10px;margin-bottom:4px">ELEMENT</div><div style="background:#1a1d2e;padding:6px 10px;border-radius:6px;color:#22d3ee;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">'+sel2+'</div>'+(txt?'<div style="color:#6b7280;font-size:10px;margin-top:3px">Text: "'+txt.slice(0,50)+'"</div>':'')+'</div>'
|
|
610
|
+
+ '<div style="margin-bottom:10px"><div style="color:#9ca3af;font-size:10px;margin-bottom:4px">TYPE</div><select id="__skopix_assert_type" style="width:100%;background:#1a1d2e;border:1px solid #374151;color:#e5e7eb;padding:6px 8px;border-radius:6px;font-family:monospace;font-size:12px"><option value="visible"'+(sugType==="visible"?" selected":"")+'>Element is visible</option><option value="text_contains"'+(sugType==="text_contains"?" selected":"")+'>Text contains</option><option value="text_equals">Text equals</option><option value="url_contains">URL contains</option><option value="element_count">Element count</option></select></div>'
|
|
611
|
+
+ '<div id="__skopix_value_row" style="margin-bottom:10px;'+(sugType==="visible"?"display:none":"")+'"><div style="color:#9ca3af;font-size:10px;margin-bottom:4px">EXPECTED VALUE</div><input id="__skopix_assert_value" type="text" value="'+sugVal.replace(/"/g,""")+'" style="width:100%;box-sizing:border-box;background:#1a1d2e;border:1px solid #374151;color:#e5e7eb;padding:6px 8px;border-radius:6px;font-family:monospace;font-size:12px"></div>'
|
|
612
|
+
+ '<div style="display:flex;gap:8px;justify-content:flex-end"><button id="__skopix_cancel_btn" style="background:transparent;border:1px solid #374151;color:#9ca3af;border-radius:6px;padding:6px 14px;cursor:pointer;font-family:monospace;font-size:12px">Cancel</button><button id="__skopix_add_btn" style="background:#1e3a5f;border:1px solid #2563eb;color:#60a5fa;border-radius:6px;padding:6px 14px;cursor:pointer;font-family:monospace;font-size:12px;font-weight:700">Add \u2713</button></div>';
|
|
613
|
+
document.body.appendChild(popover);
|
|
614
|
+
popover.querySelector('#__skopix_assert_type').addEventListener('change', function() {
|
|
615
|
+
const t = this.value; popover.querySelector('#__skopix_value_row').style.display = t==='visible'?'none':'';
|
|
616
|
+
});
|
|
617
|
+
popover.querySelector('#__skopix_cancel_btn').addEventListener('click', function(e3) { e3.stopPropagation(); hl.style.display='none'; popover.remove(); });
|
|
618
|
+
popover.querySelector('#__skopix_add_btn').addEventListener('click', function(e3) {
|
|
619
|
+
e3.stopPropagation();
|
|
620
|
+
const assertType = popover.querySelector('#__skopix_assert_type').value;
|
|
621
|
+
const value = popover.querySelector('#__skopix_assert_value').value.trim();
|
|
622
|
+
if (assertType !== 'visible' && assertType !== 'url_contains' && !value) { popover.querySelector('#__skopix_assert_value').style.borderColor='#dc2626'; return; }
|
|
623
|
+
window.__skopixCapture && window.__skopixCapture({ action:'assert', assertType, selector:assertType==='url_contains'?null:sel2, value:value||null, element:assertType==='url_contains'?null:{tag:el2.tagName.toLowerCase(),text:txt} });
|
|
624
|
+
hl.style.display='none'; popover.remove();
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
document.addEventListener('keydown', function escH(e2) { if (e2.key==='Escape') { window.__skopixPickMode=false; document.body.style.cursor=''; overlay.remove(); const h2=document.getElementById('__skopix_hint');if(h2)h2.remove(); hl.style.display='none'; document.removeEventListener('keydown',escH); } });
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Inject toolbar into the CURRENT page immediately (addInitScript only fires on nav)
|
|
485
632
|
await page.evaluate(() => {
|
|
486
633
|
if (window.__skopixRecording) return;
|
|
487
634
|
window.__skopixRecording = true;
|
|
@@ -628,9 +775,9 @@ export async function agentCommand(options) {
|
|
|
628
775
|
});
|
|
629
776
|
});
|
|
630
777
|
|
|
631
|
-
// Tell dashboard
|
|
778
|
+
// Tell dashboard replay is done — frontend closes replay SSE and opens recording SSE
|
|
632
779
|
sendRun({ type: 'done', exitCode: 0, status: 'passed', recordingId });
|
|
633
|
-
process.stderr.write('[debug] toolbar injected,
|
|
780
|
+
process.stderr.write('[debug] toolbar injected, recording live, awaiting user actions\n');
|
|
634
781
|
|
|
635
782
|
} catch (err) {
|
|
636
783
|
console.error(chalk.red(' \u2716 Debug error: ' + err.message));
|
package/package.json
CHANGED