@ytspar/sweetlink 1.19.0 → 1.21.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/README.md +40 -0
- package/dist/cli/outputSchemas.d.ts +57 -1
- package/dist/cli/outputSchemas.d.ts.map +1 -1
- package/dist/cli/outputSchemas.js +36 -1
- package/dist/cli/outputSchemas.js.map +1 -1
- package/dist/cli/sweetlink.js +400 -48
- package/dist/cli/sweetlink.js.map +1 -1
- package/dist/daemon/browser.d.ts.map +1 -1
- package/dist/daemon/browser.js.map +1 -1
- package/dist/daemon/client.d.ts +7 -0
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +16 -2
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/demo.d.ts.map +1 -1
- package/dist/daemon/demo.js +6 -2
- package/dist/daemon/demo.js.map +1 -1
- package/dist/daemon/diff.d.ts.map +1 -1
- package/dist/daemon/diff.js +5 -3
- package/dist/daemon/diff.js.map +1 -1
- package/dist/daemon/evidence.d.ts.map +1 -1
- package/dist/daemon/evidence.js +6 -6
- package/dist/daemon/evidence.js.map +1 -1
- package/dist/daemon/index.js +1 -1
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/listeners.d.ts.map +1 -1
- package/dist/daemon/listeners.js +8 -6
- package/dist/daemon/listeners.js.map +1 -1
- package/dist/daemon/recording.d.ts +5 -0
- package/dist/daemon/recording.d.ts.map +1 -1
- package/dist/daemon/recording.js +38 -15
- package/dist/daemon/recording.js.map +1 -1
- package/dist/daemon/refs.d.ts.map +1 -1
- package/dist/daemon/refs.js +1 -1
- package/dist/daemon/refs.js.map +1 -1
- package/dist/daemon/ringBuffer.d.ts +8 -0
- package/dist/daemon/ringBuffer.d.ts.map +1 -1
- package/dist/daemon/ringBuffer.js +17 -0
- package/dist/daemon/ringBuffer.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +490 -86
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/stateFile.js +2 -2
- package/dist/daemon/stateFile.js.map +1 -1
- package/dist/daemon/summary.d.ts +1 -1
- package/dist/daemon/summary.d.ts.map +1 -1
- package/dist/daemon/summary.js +2 -2
- package/dist/daemon/summary.js.map +1 -1
- package/dist/daemon/types.d.ts +1 -1
- package/dist/daemon/types.d.ts.map +1 -1
- package/dist/daemon/types.js.map +1 -1
- package/dist/daemon/viewer.d.ts +1 -1
- package/dist/daemon/viewer.d.ts.map +1 -1
- package/dist/daemon/viewer.js +21 -13
- package/dist/daemon/viewer.js.map +1 -1
- package/dist/daemon/visualDiff.js +1 -1
- package/dist/daemon/visualDiff.js.map +1 -1
- package/dist/next.js +3 -3
- package/dist/next.js.map +1 -1
- package/dist/ruler.js +3 -1
- package/dist/ruler.js.map +1 -1
- package/dist/runs.d.ts +34 -0
- package/dist/runs.d.ts.map +1 -0
- package/dist/runs.js +61 -0
- package/dist/runs.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +20 -10
- package/dist/server/index.js.map +1 -1
- package/dist/simulator/android.d.ts +13 -0
- package/dist/simulator/android.d.ts.map +1 -1
- package/dist/simulator/android.js +75 -5
- package/dist/simulator/android.js.map +1 -1
- package/dist/simulator/androidTaps.d.ts +99 -0
- package/dist/simulator/androidTaps.d.ts.map +1 -0
- package/dist/simulator/androidTaps.js +162 -0
- package/dist/simulator/androidTaps.js.map +1 -0
- package/dist/simulator/ios.d.ts.map +1 -1
- package/dist/simulator/ios.js +13 -5
- package/dist/simulator/ios.js.map +1 -1
- package/dist/simulator/overlay.d.ts +41 -0
- package/dist/simulator/overlay.d.ts.map +1 -0
- package/dist/simulator/overlay.js +78 -0
- package/dist/simulator/overlay.js.map +1 -0
- package/dist/term/ansi.d.ts.map +1 -1
- package/dist/term/ansi.js +49 -14
- package/dist/term/ansi.js.map +1 -1
- package/dist/term/player.d.ts.map +1 -1
- package/dist/term/player.js +5 -5
- package/dist/term/player.js.map +1 -1
- package/dist/term/recorder.js +1 -1
- package/dist/term/recorder.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +8 -4
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon/server.js
CHANGED
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
* Manages idle timer for auto-shutdown.
|
|
7
7
|
*/
|
|
8
8
|
import { createServer } from 'http';
|
|
9
|
-
import { closeBrowser, getBrowserInstance, getPage, initBrowser, takeResponsiveScreenshots, takeScreenshot } from './browser.js';
|
|
10
|
-
import { annotateScreenshot, diffSnapshots } from './diff.js';
|
|
9
|
+
import { closeBrowser, getBrowserInstance, getPage, initBrowser, takeResponsiveScreenshots, takeScreenshot, } from './browser.js';
|
|
11
10
|
import { takeDeviceScreenshots } from './devices.js';
|
|
12
|
-
import {
|
|
13
|
-
import { getRecordingPage, getRecordingStatus, isRecording, logAction, pauseRecording, resumeRecording, startRecording, stopRecording } from './recording.js';
|
|
11
|
+
import { annotateScreenshot, diffSnapshots } from './diff.js';
|
|
14
12
|
import { detectServerErrors } from './errorPatterns.js';
|
|
13
|
+
import { consoleBuffer, dialogBuffer, formatConsoleEntries, formatNetworkEntries, getErrorCount, getWarningCount, networkBuffer, } from './listeners.js';
|
|
14
|
+
import { getRecordingEventCursors, getRecordingPage, getRecordingStatus, isRecording, logAction, pauseRecording, resumeRecording, startRecording, stopRecording, } from './recording.js';
|
|
15
|
+
import { buildRefMap, checkRefStale, formatRefMap, getBaseline, getCurrentRefMap, resolveRef, setBaseline, } from './refs.js';
|
|
15
16
|
import { generateSummary } from './summary.js';
|
|
17
|
+
import { DAEMON_IDLE_TIMEOUT_MS, DEFAULT_RESPONSIVE_VIEWPORTS } from './types.js';
|
|
16
18
|
import { generateViewer } from './viewer.js';
|
|
17
19
|
import { visualDiff } from './visualDiff.js';
|
|
18
|
-
import { buildRefMap, checkRefStale, formatRefMap, getBaseline, getCurrentRefMap, resolveRef, setBaseline, } from './refs.js';
|
|
19
|
-
import { DAEMON_IDLE_TIMEOUT_MS, DEFAULT_RESPONSIVE_VIEWPORTS } from './types.js';
|
|
20
20
|
// ============================================================================
|
|
21
21
|
// State
|
|
22
22
|
// ============================================================================
|
|
@@ -197,7 +197,10 @@ async function captureFailure(page, reason) {
|
|
|
197
197
|
const dir = '.sweetlink/failures';
|
|
198
198
|
await fsp.mkdir(dir, { recursive: true });
|
|
199
199
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
200
|
-
const slug = reason
|
|
200
|
+
const slug = reason
|
|
201
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
202
|
+
.slice(0, 40)
|
|
203
|
+
.toLowerCase();
|
|
201
204
|
const filePath = `${dir}/${stamp}-${slug}.png`;
|
|
202
205
|
const buf = await page.screenshot({ fullPage: false });
|
|
203
206
|
await fsp.writeFile(filePath, buf);
|
|
@@ -207,6 +210,314 @@ async function captureFailure(page, reason) {
|
|
|
207
210
|
return undefined;
|
|
208
211
|
}
|
|
209
212
|
}
|
|
213
|
+
function failureData(page, details, failureScreenshot) {
|
|
214
|
+
return {
|
|
215
|
+
currentUrl: page.url(),
|
|
216
|
+
...details,
|
|
217
|
+
...(failureScreenshot ? { failureScreenshot } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function slugifyArtifact(value) {
|
|
221
|
+
return (value
|
|
222
|
+
.toLowerCase()
|
|
223
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
224
|
+
.replace(/^-|-$/g, '')
|
|
225
|
+
.slice(0, 72) || 'inspect');
|
|
226
|
+
}
|
|
227
|
+
function normalizeActionTranscript(value) {
|
|
228
|
+
if (!Array.isArray(value))
|
|
229
|
+
return [];
|
|
230
|
+
return value.flatMap((entry) => {
|
|
231
|
+
if (!entry || typeof entry !== 'object')
|
|
232
|
+
return [];
|
|
233
|
+
const record = entry;
|
|
234
|
+
const action = typeof record.action === 'string' ? record.action : undefined;
|
|
235
|
+
if (!action)
|
|
236
|
+
return [];
|
|
237
|
+
return [
|
|
238
|
+
{
|
|
239
|
+
action,
|
|
240
|
+
target: typeof record.target === 'string' ? record.target : undefined,
|
|
241
|
+
result: typeof record.result === 'string' ? record.result : undefined,
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async function getPageInfo(page) {
|
|
247
|
+
return page.evaluate(() => {
|
|
248
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
249
|
+
const fcp = performance.getEntriesByName('first-contentful-paint')[0];
|
|
250
|
+
return {
|
|
251
|
+
url: location.href,
|
|
252
|
+
title: document.title,
|
|
253
|
+
viewport: {
|
|
254
|
+
width: window.innerWidth,
|
|
255
|
+
height: window.innerHeight,
|
|
256
|
+
deviceScaleFactor: window.devicePixelRatio,
|
|
257
|
+
},
|
|
258
|
+
vitals: {
|
|
259
|
+
fcp: fcp ? Math.round(fcp.startTime) : null,
|
|
260
|
+
pageSize: nav ? nav.transferSize || nav.encodedBodySize || null : null,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async function getAxeSource() {
|
|
266
|
+
try {
|
|
267
|
+
const axeModule = await import('axe-core');
|
|
268
|
+
const candidate = axeModule;
|
|
269
|
+
return candidate.source ?? candidate.default?.source ?? null;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function runInspectA11y(page) {
|
|
276
|
+
const source = await getAxeSource();
|
|
277
|
+
if (!source) {
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
error: 'axe-core is not available in this environment',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
await page.addScriptTag({ content: source });
|
|
285
|
+
return await page.evaluate(async () => {
|
|
286
|
+
const axe = window.axe;
|
|
287
|
+
if (!axe)
|
|
288
|
+
throw new Error('axe-core did not initialize on the page');
|
|
289
|
+
const compactIssue = (issue) => ({
|
|
290
|
+
id: issue.id,
|
|
291
|
+
impact: issue.impact ?? 'unknown',
|
|
292
|
+
help: issue.help ?? '',
|
|
293
|
+
description: issue.description ?? '',
|
|
294
|
+
helpUrl: issue.helpUrl ?? '',
|
|
295
|
+
nodes: (issue.nodes ?? []).slice(0, 3).map((node) => ({
|
|
296
|
+
target: (node.target ?? []).join(' '),
|
|
297
|
+
html: (node.html ?? '').slice(0, 180),
|
|
298
|
+
summary: node.failureSummary ??
|
|
299
|
+
node.any?.[0]?.message ??
|
|
300
|
+
node.all?.[0]?.message ??
|
|
301
|
+
node.none?.[0]?.message ??
|
|
302
|
+
'',
|
|
303
|
+
})),
|
|
304
|
+
});
|
|
305
|
+
const result = await axe.run(document, {
|
|
306
|
+
runOnly: {
|
|
307
|
+
type: 'tag',
|
|
308
|
+
values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'],
|
|
309
|
+
},
|
|
310
|
+
rules: {
|
|
311
|
+
'color-contrast': { enabled: true },
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
const byImpact = {};
|
|
315
|
+
for (const violation of result.violations) {
|
|
316
|
+
const impact = violation.impact ?? 'unknown';
|
|
317
|
+
byImpact[impact] = (byImpact[impact] ?? 0) + 1;
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
ok: true,
|
|
321
|
+
summary: {
|
|
322
|
+
violations: result.violations.length,
|
|
323
|
+
incomplete: result.incomplete.length,
|
|
324
|
+
passes: result.passes.length,
|
|
325
|
+
byImpact,
|
|
326
|
+
},
|
|
327
|
+
violations: result.violations.map(compactIssue),
|
|
328
|
+
incomplete: result.incomplete.map(compactIssue),
|
|
329
|
+
};
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
return {
|
|
334
|
+
ok: false,
|
|
335
|
+
error: error instanceof Error ? error.message : String(error),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function inspectNextActions(counts, artifacts) {
|
|
340
|
+
const actions = [
|
|
341
|
+
`Open ${artifacts.summaryMarkdown} and ${artifacts.screenshotPng} before making visual claims.`,
|
|
342
|
+
];
|
|
343
|
+
if (counts.consoleErrors > 0) {
|
|
344
|
+
actions.push('Investigate console errors before changing UI behavior.');
|
|
345
|
+
}
|
|
346
|
+
else if (counts.consoleWarnings > 0) {
|
|
347
|
+
actions.push('Review console warnings for stale props, hydration, or deprecated API signals.');
|
|
348
|
+
}
|
|
349
|
+
if (counts.networkFailures > 0) {
|
|
350
|
+
actions.push('Inspect failed network entries before assuming the UI state is a component bug.');
|
|
351
|
+
}
|
|
352
|
+
if ((counts.a11yViolations ?? 0) > 0 || (counts.a11yIncomplete ?? 0) > 0) {
|
|
353
|
+
actions.push(`Use ${artifacts.a11yJson ?? 'the a11y artifact'} to prioritize accessibility fixes.`);
|
|
354
|
+
}
|
|
355
|
+
if (counts.refs > 0) {
|
|
356
|
+
actions.push('Use @e refs for click/fill/press actions, then rerun inspect after DOM changes.');
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
actions.push('No interactive refs were found; inspect the DOM/outline before attempting actions.');
|
|
360
|
+
}
|
|
361
|
+
return actions;
|
|
362
|
+
}
|
|
363
|
+
function renderInspectSummary(data) {
|
|
364
|
+
const transcript = data.actionTranscript.length > 0
|
|
365
|
+
? data.actionTranscript
|
|
366
|
+
.map((entry, index) => {
|
|
367
|
+
const target = entry.target ? ` target=${entry.target}` : '';
|
|
368
|
+
const result = entry.result ? ` result=${entry.result}` : '';
|
|
369
|
+
return `${index + 1}. ${entry.action}${target}${result}`;
|
|
370
|
+
})
|
|
371
|
+
.join('\n')
|
|
372
|
+
: '(none supplied)';
|
|
373
|
+
const refs = data.refs.length > 0
|
|
374
|
+
? data.refs.map((ref) => `- ${ref.ref} [${ref.role}] "${ref.name}"`).join('\n')
|
|
375
|
+
: '(no interactive refs)';
|
|
376
|
+
const a11ySummary = data.a11y
|
|
377
|
+
? JSON.stringify(data.a11y.summary ?? { ok: data.a11y.ok, error: data.a11y.error }, null, 2)
|
|
378
|
+
: '(skipped)';
|
|
379
|
+
return [
|
|
380
|
+
'# Sweetlink Inspect',
|
|
381
|
+
'',
|
|
382
|
+
`- URL: ${data.url}`,
|
|
383
|
+
`- Title: ${data.title || '(untitled)'}`,
|
|
384
|
+
`- Generated: ${data.generatedAt}`,
|
|
385
|
+
`- Viewport: ${data.viewport.width}x${data.viewport.height} @${data.viewport.deviceScaleFactor}x`,
|
|
386
|
+
`- FCP: ${data.vitals.fcp ?? 'n/a'}ms`,
|
|
387
|
+
`- Page size: ${data.vitals.pageSize ?? 'n/a'} bytes`,
|
|
388
|
+
'',
|
|
389
|
+
'## Expected Outcome',
|
|
390
|
+
'',
|
|
391
|
+
data.expectedOutcome ?? '(not supplied)',
|
|
392
|
+
'',
|
|
393
|
+
'## Action Transcript',
|
|
394
|
+
'',
|
|
395
|
+
transcript,
|
|
396
|
+
'',
|
|
397
|
+
'## Counts',
|
|
398
|
+
'',
|
|
399
|
+
`- Interactive refs: ${data.counts.refs}`,
|
|
400
|
+
`- Console: ${data.counts.consoleEntries} entries, ${data.counts.consoleErrors} errors, ${data.counts.consoleWarnings} warnings`,
|
|
401
|
+
`- Network: ${data.counts.networkEntries} entries, ${data.counts.networkFailures} failures`,
|
|
402
|
+
`- Accessibility: ${data.counts.a11yViolations ?? 'n/a'} violations, ${data.counts.a11yIncomplete ?? 'n/a'} incomplete`,
|
|
403
|
+
'',
|
|
404
|
+
'## Next Actions',
|
|
405
|
+
'',
|
|
406
|
+
data.nextActions.map((action) => `- ${action}`).join('\n'),
|
|
407
|
+
'',
|
|
408
|
+
'## Artifacts',
|
|
409
|
+
'',
|
|
410
|
+
...Object.entries(data.artifacts).map(([key, value]) => `- ${key}: ${value ?? '(not generated)'}`),
|
|
411
|
+
'',
|
|
412
|
+
'## Interactive Refs',
|
|
413
|
+
'',
|
|
414
|
+
refs,
|
|
415
|
+
'',
|
|
416
|
+
'## Console',
|
|
417
|
+
'',
|
|
418
|
+
data.consoleText,
|
|
419
|
+
'',
|
|
420
|
+
'## Network',
|
|
421
|
+
'',
|
|
422
|
+
data.networkText,
|
|
423
|
+
'',
|
|
424
|
+
'## Accessibility',
|
|
425
|
+
'',
|
|
426
|
+
a11ySummary,
|
|
427
|
+
'',
|
|
428
|
+
].join('\n');
|
|
429
|
+
}
|
|
430
|
+
async function handleInspect(params, url) {
|
|
431
|
+
await initBrowser(url);
|
|
432
|
+
const page = getRecordingPage() ?? getPage();
|
|
433
|
+
const pageInfo = await getPageInfo(page);
|
|
434
|
+
const generatedAt = new Date().toISOString();
|
|
435
|
+
const label = typeof params.label === 'string' ? params.label : 'inspect';
|
|
436
|
+
const stamp = generatedAt.replace(/[:.]/g, '-');
|
|
437
|
+
const { promises: fsp } = await import('fs');
|
|
438
|
+
const path = await import('path');
|
|
439
|
+
const dir = path.resolve(`.sweetlink/inspect/${stamp}-${slugifyArtifact(label)}`);
|
|
440
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
441
|
+
const lastRaw = typeof params.last === 'number' ? params.last : Number(params.last);
|
|
442
|
+
const last = Number.isFinite(lastRaw) && lastRaw > 0 ? Math.min(Math.floor(lastRaw), 500) : 50;
|
|
443
|
+
const includeA11y = params.includeA11y !== false;
|
|
444
|
+
const expectedOutcome = typeof params.expectedOutcome === 'string' ? params.expectedOutcome : undefined;
|
|
445
|
+
const actionTranscript = normalizeActionTranscript(params.actionTranscript);
|
|
446
|
+
const refMap = await buildRefMap(page, { interactive: true });
|
|
447
|
+
const snapshotText = formatRefMap(refMap);
|
|
448
|
+
const screenshotBuffer = await page.screenshot({ fullPage: true });
|
|
449
|
+
const consoleEntries = consoleBuffer.toArray().slice(-last);
|
|
450
|
+
const networkEntries = networkBuffer.toArray().slice(-last);
|
|
451
|
+
const consoleText = formatConsoleEntries(consoleEntries);
|
|
452
|
+
const networkText = formatNetworkEntries(networkEntries);
|
|
453
|
+
const a11y = includeA11y ? await runInspectA11y(page) : undefined;
|
|
454
|
+
const artifacts = {
|
|
455
|
+
dir,
|
|
456
|
+
summaryMarkdown: path.join(dir, 'SUMMARY.md'),
|
|
457
|
+
contextJson: path.join(dir, 'context.json'),
|
|
458
|
+
screenshotPng: path.join(dir, 'screenshot.png'),
|
|
459
|
+
snapshotMarkdown: path.join(dir, 'snapshot.md'),
|
|
460
|
+
consoleText: path.join(dir, 'console.txt'),
|
|
461
|
+
networkText: path.join(dir, 'network.txt'),
|
|
462
|
+
a11yJson: includeA11y ? path.join(dir, 'a11y.json') : undefined,
|
|
463
|
+
};
|
|
464
|
+
const a11ySummary = a11y?.summary;
|
|
465
|
+
const counts = {
|
|
466
|
+
refs: refMap.entries.length,
|
|
467
|
+
consoleEntries: consoleEntries.length,
|
|
468
|
+
consoleErrors: consoleEntries.filter((entry) => entry.level === 'error').length,
|
|
469
|
+
consoleWarnings: consoleEntries.filter((entry) => entry.level === 'warning').length,
|
|
470
|
+
networkEntries: networkEntries.length,
|
|
471
|
+
networkFailures: networkEntries.filter((entry) => entry.status >= 400 || entry.status === 0)
|
|
472
|
+
.length,
|
|
473
|
+
a11yViolations: a11ySummary?.violations,
|
|
474
|
+
a11yIncomplete: a11ySummary?.incomplete,
|
|
475
|
+
};
|
|
476
|
+
const nextActions = inspectNextActions(counts, artifacts);
|
|
477
|
+
const context = {
|
|
478
|
+
url: pageInfo.url,
|
|
479
|
+
title: pageInfo.title,
|
|
480
|
+
generatedAt,
|
|
481
|
+
viewport: pageInfo.viewport,
|
|
482
|
+
vitals: pageInfo.vitals,
|
|
483
|
+
artifacts,
|
|
484
|
+
counts,
|
|
485
|
+
refs: refMap.entries,
|
|
486
|
+
console: { entries: consoleEntries, formatted: consoleText },
|
|
487
|
+
network: { entries: networkEntries, formatted: networkText },
|
|
488
|
+
a11y,
|
|
489
|
+
expectedOutcome,
|
|
490
|
+
actionTranscript,
|
|
491
|
+
failureArtifacts: [],
|
|
492
|
+
nextActions,
|
|
493
|
+
};
|
|
494
|
+
await fsp.writeFile(artifacts.screenshotPng, screenshotBuffer);
|
|
495
|
+
await fsp.writeFile(artifacts.snapshotMarkdown, snapshotText, 'utf-8');
|
|
496
|
+
await fsp.writeFile(artifacts.consoleText, consoleText, 'utf-8');
|
|
497
|
+
await fsp.writeFile(artifacts.networkText, networkText, 'utf-8');
|
|
498
|
+
if (artifacts.a11yJson) {
|
|
499
|
+
await fsp.writeFile(artifacts.a11yJson, JSON.stringify(a11y ?? null, null, 2), 'utf-8');
|
|
500
|
+
}
|
|
501
|
+
const summary = renderInspectSummary({
|
|
502
|
+
url: context.url,
|
|
503
|
+
title: context.title,
|
|
504
|
+
generatedAt: context.generatedAt,
|
|
505
|
+
viewport: context.viewport,
|
|
506
|
+
vitals: context.vitals,
|
|
507
|
+
artifacts: context.artifacts,
|
|
508
|
+
counts: context.counts,
|
|
509
|
+
refs: context.refs,
|
|
510
|
+
consoleText,
|
|
511
|
+
networkText,
|
|
512
|
+
a11y,
|
|
513
|
+
expectedOutcome,
|
|
514
|
+
actionTranscript,
|
|
515
|
+
nextActions,
|
|
516
|
+
});
|
|
517
|
+
await fsp.writeFile(artifacts.summaryMarkdown, summary, 'utf-8');
|
|
518
|
+
await fsp.writeFile(artifacts.contextJson, JSON.stringify(context, null, 2), 'utf-8');
|
|
519
|
+
return { ok: true, data: context };
|
|
520
|
+
}
|
|
210
521
|
async function handleClickRef(params, url) {
|
|
211
522
|
await initBrowser(url);
|
|
212
523
|
// Use recording page if recording, otherwise main page
|
|
@@ -221,7 +532,12 @@ async function handleClickRef(params, url) {
|
|
|
221
532
|
return {
|
|
222
533
|
ok: false,
|
|
223
534
|
error: `Ref ${ref} is stale — element no longer exists. Run \`snapshot\` to get fresh refs.`,
|
|
224
|
-
data:
|
|
535
|
+
data: failureData(page, {
|
|
536
|
+
action: 'click-ref',
|
|
537
|
+
ref,
|
|
538
|
+
staleRef: true,
|
|
539
|
+
remediation: 'Run `sweetlink snapshot -i` or `sweetlink inspect` to refresh refs before retrying.',
|
|
540
|
+
}, failureScreenshot),
|
|
225
541
|
};
|
|
226
542
|
}
|
|
227
543
|
const locator = resolveRef(page, ref);
|
|
@@ -233,12 +549,31 @@ async function handleClickRef(params, url) {
|
|
|
233
549
|
return {
|
|
234
550
|
ok: false,
|
|
235
551
|
error: `Ref ${ref} is disabled — cannot click.`,
|
|
236
|
-
data:
|
|
552
|
+
data: failureData(page, {
|
|
553
|
+
action: 'click-ref',
|
|
554
|
+
ref,
|
|
555
|
+
disabled: true,
|
|
556
|
+
remediation: 'Wait for the control to become enabled or choose a different interactive ref.',
|
|
557
|
+
}, failureScreenshot),
|
|
237
558
|
};
|
|
238
559
|
}
|
|
239
560
|
const box = await locator.boundingBox();
|
|
240
561
|
const t0 = Date.now();
|
|
241
|
-
|
|
562
|
+
try {
|
|
563
|
+
await locator.click();
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
const failureScreenshot = await captureFailure(page, `click-failed-${ref}`);
|
|
567
|
+
return {
|
|
568
|
+
ok: false,
|
|
569
|
+
error: `Click failed for ${ref}: ${error instanceof Error ? error.message : String(error)}`,
|
|
570
|
+
data: failureData(page, {
|
|
571
|
+
action: 'click-ref',
|
|
572
|
+
ref,
|
|
573
|
+
remediation: 'Inspect the failure screenshot and rerun `sweetlink snapshot -i` if the layout changed.',
|
|
574
|
+
}, failureScreenshot),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
242
577
|
const durationMs = Date.now() - t0;
|
|
243
578
|
// Log action if recording
|
|
244
579
|
if (isRecording()) {
|
|
@@ -258,9 +593,16 @@ async function handleFillRef(params, url) {
|
|
|
258
593
|
return { ok: false, error: 'Missing value parameter' };
|
|
259
594
|
const stale = await checkRefStale(page, ref);
|
|
260
595
|
if (stale) {
|
|
596
|
+
const failureScreenshot = await captureFailure(page, `stale-fill-ref-${ref}`);
|
|
261
597
|
return {
|
|
262
598
|
ok: false,
|
|
263
599
|
error: `Ref ${ref} is stale — element no longer exists. Run \`snapshot\` to get fresh refs.`,
|
|
600
|
+
data: failureData(page, {
|
|
601
|
+
action: 'fill-ref',
|
|
602
|
+
ref,
|
|
603
|
+
staleRef: true,
|
|
604
|
+
remediation: 'Run `sweetlink snapshot -i` or `sweetlink inspect` to refresh refs before retrying.',
|
|
605
|
+
}, failureScreenshot),
|
|
264
606
|
};
|
|
265
607
|
}
|
|
266
608
|
const locator = resolveRef(page, ref);
|
|
@@ -272,12 +614,31 @@ async function handleFillRef(params, url) {
|
|
|
272
614
|
return {
|
|
273
615
|
ok: false,
|
|
274
616
|
error: `Ref ${ref} is not editable (use click-ref/press-key for non-text inputs).`,
|
|
275
|
-
data:
|
|
617
|
+
data: failureData(page, {
|
|
618
|
+
action: 'fill-ref',
|
|
619
|
+
ref,
|
|
620
|
+
nonEditable: true,
|
|
621
|
+
remediation: 'Use `fill-ref` only on editable inputs, textareas, or contenteditable fields.',
|
|
622
|
+
}, failureScreenshot),
|
|
276
623
|
};
|
|
277
624
|
}
|
|
278
625
|
const box = await locator.boundingBox();
|
|
279
626
|
const t0 = Date.now();
|
|
280
|
-
|
|
627
|
+
try {
|
|
628
|
+
await locator.fill(value);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
const failureScreenshot = await captureFailure(page, `fill-failed-${ref}`);
|
|
632
|
+
return {
|
|
633
|
+
ok: false,
|
|
634
|
+
error: `Fill failed for ${ref}: ${error instanceof Error ? error.message : String(error)}`,
|
|
635
|
+
data: failureData(page, {
|
|
636
|
+
action: 'fill-ref',
|
|
637
|
+
ref,
|
|
638
|
+
remediation: 'Inspect the failure screenshot and rerun `sweetlink snapshot -i` if the target changed.',
|
|
639
|
+
}, failureScreenshot),
|
|
640
|
+
};
|
|
641
|
+
}
|
|
281
642
|
const durationMs = Date.now() - t0;
|
|
282
643
|
if (isRecording()) {
|
|
283
644
|
await logAction('fill', [ref, value], page, box ?? undefined, durationMs);
|
|
@@ -316,7 +677,14 @@ async function handleClickCss(params, url) {
|
|
|
316
677
|
return {
|
|
317
678
|
ok: false,
|
|
318
679
|
error: `No element found matching: ${selector ?? text} (${found} matches)`,
|
|
319
|
-
data:
|
|
680
|
+
data: failureData(page, {
|
|
681
|
+
action: 'click-css',
|
|
682
|
+
selector,
|
|
683
|
+
text,
|
|
684
|
+
index,
|
|
685
|
+
matchCount: found,
|
|
686
|
+
remediation: 'Update the selector/text or run `sweetlink inspect` to verify the current DOM.',
|
|
687
|
+
}, failureScreenshot),
|
|
320
688
|
};
|
|
321
689
|
}
|
|
322
690
|
const box = await target.boundingBox();
|
|
@@ -334,11 +702,13 @@ async function handleClickCss(params, url) {
|
|
|
334
702
|
return { ours: true };
|
|
335
703
|
if (top === intended || intended.contains(top))
|
|
336
704
|
return { ours: true };
|
|
337
|
-
const desc =
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
705
|
+
const desc = [
|
|
706
|
+
top.tagName.toLowerCase(),
|
|
707
|
+
top.id ? `#${top.id}` : '',
|
|
708
|
+
top.className && typeof top.className === 'string'
|
|
709
|
+
? `.${top.className.split(/\s+/).filter(Boolean).slice(0, 2).join('.')}`
|
|
710
|
+
: '',
|
|
711
|
+
].join('');
|
|
342
712
|
return { ours: false, coveredBy: desc };
|
|
343
713
|
}, { cx, cy, sel: selector ?? null });
|
|
344
714
|
if (!info.ours) {
|
|
@@ -346,12 +716,35 @@ async function handleClickCss(params, url) {
|
|
|
346
716
|
return {
|
|
347
717
|
ok: false,
|
|
348
718
|
error: `Click target ${selector ?? text} is covered by <${info.coveredBy}>. Dismiss the overlay first.`,
|
|
349
|
-
data:
|
|
719
|
+
data: failureData(page, {
|
|
720
|
+
action: 'click-css',
|
|
721
|
+
selector,
|
|
722
|
+
text,
|
|
723
|
+
index,
|
|
724
|
+
coveredBy: info.coveredBy,
|
|
725
|
+
remediation: 'Scroll, close overlays, or click a visible target before retrying.',
|
|
726
|
+
}, failureScreenshot),
|
|
350
727
|
};
|
|
351
728
|
}
|
|
352
729
|
}
|
|
353
730
|
const t0 = Date.now();
|
|
354
|
-
|
|
731
|
+
try {
|
|
732
|
+
await target.click();
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
const failureScreenshot = await captureFailure(page, `click-css-failed-${selector ?? text}`);
|
|
736
|
+
return {
|
|
737
|
+
ok: false,
|
|
738
|
+
error: `Click failed for ${selector ?? text}: ${error instanceof Error ? error.message : String(error)}`,
|
|
739
|
+
data: failureData(page, {
|
|
740
|
+
action: 'click-css',
|
|
741
|
+
selector,
|
|
742
|
+
text,
|
|
743
|
+
index,
|
|
744
|
+
remediation: 'Inspect the failure screenshot and rerun `sweetlink inspect` if the layout changed.',
|
|
745
|
+
}, failureScreenshot),
|
|
746
|
+
};
|
|
747
|
+
}
|
|
355
748
|
const durationMs = Date.now() - t0;
|
|
356
749
|
const tag = await target.evaluate((el) => el.tagName.toLowerCase()).catch(() => 'unknown');
|
|
357
750
|
const found = await locator.count();
|
|
@@ -499,11 +892,15 @@ async function handleRecordStart(params, url) {
|
|
|
499
892
|
const storageState = params.storageState || undefined;
|
|
500
893
|
const trace = params.trace || undefined;
|
|
501
894
|
const result = await startRecording(browser, url, '.sweetlink', {
|
|
502
|
-
viewport,
|
|
895
|
+
viewport,
|
|
896
|
+
label,
|
|
897
|
+
storageState,
|
|
898
|
+
trace,
|
|
503
899
|
});
|
|
504
900
|
return { ok: true, data: { sessionId: result.sessionId, label, trace: !!trace } };
|
|
505
901
|
}
|
|
506
902
|
async function handleRecordStop() {
|
|
903
|
+
const eventCursors = getRecordingEventCursors();
|
|
507
904
|
const manifest = await stopRecording();
|
|
508
905
|
if (!manifest) {
|
|
509
906
|
return { ok: false, error: 'No recording in progress' };
|
|
@@ -513,8 +910,12 @@ async function handleRecordStop() {
|
|
|
513
910
|
let viewerPath;
|
|
514
911
|
let summaryPath;
|
|
515
912
|
try {
|
|
516
|
-
const consoleLogs =
|
|
517
|
-
|
|
913
|
+
const consoleLogs = eventCursors
|
|
914
|
+
? consoleBuffer.since(eventCursors.consoleStartCursor)
|
|
915
|
+
: consoleBuffer.toArray();
|
|
916
|
+
const networkLogs = eventCursors
|
|
917
|
+
? networkBuffer.since(eventCursors.networkStartCursor)
|
|
918
|
+
: networkBuffer.toArray();
|
|
518
919
|
viewerPath = await generateViewer(manifest, {
|
|
519
920
|
sessionDir,
|
|
520
921
|
consoleEntries: consoleLogs,
|
|
@@ -523,16 +924,17 @@ async function handleRecordStop() {
|
|
|
523
924
|
// Generate SUMMARY.md
|
|
524
925
|
const { promises: fsp } = await import('fs');
|
|
525
926
|
// Detect server errors from console log messages
|
|
526
|
-
const consoleText = consoleLogs.map(e => e.message).join('\n');
|
|
927
|
+
const consoleText = consoleLogs.map((e) => e.message).join('\n');
|
|
527
928
|
const serverErrors = detectServerErrors(consoleText);
|
|
528
929
|
if (serverErrors.length > 0) {
|
|
529
930
|
manifest.errors.server = serverErrors.length;
|
|
530
931
|
}
|
|
932
|
+
await fsp.writeFile(`${sessionDir}/sweetlink-session.json`, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
531
933
|
const summaryMd = generateSummary({
|
|
532
934
|
manifest,
|
|
533
935
|
consoleEntries: consoleLogs,
|
|
534
936
|
networkEntries: networkLogs,
|
|
535
|
-
serverErrors: serverErrors.map(e => ({
|
|
937
|
+
serverErrors: serverErrors.map((e) => ({
|
|
536
938
|
source: 'server',
|
|
537
939
|
message: e.line,
|
|
538
940
|
timestamp: Date.now(),
|
|
@@ -549,7 +951,9 @@ async function handleRecordStop() {
|
|
|
549
951
|
console.error('[Daemon] Report generation error:', e);
|
|
550
952
|
}
|
|
551
953
|
// Include a browser-accessible URL for the viewer
|
|
552
|
-
const viewerUrl = manifest.sessionId && daemonPort
|
|
954
|
+
const viewerUrl = manifest.sessionId && daemonPort
|
|
955
|
+
? `http://127.0.0.1:${daemonPort}/viewer/${manifest.sessionId}`
|
|
956
|
+
: undefined;
|
|
553
957
|
return { ok: true, data: { manifest, viewerPath, viewerUrl, summaryPath } };
|
|
554
958
|
}
|
|
555
959
|
async function handleRecordStatus() {
|
|
@@ -588,8 +992,14 @@ async function handleSessionsList() {
|
|
|
588
992
|
try {
|
|
589
993
|
const raw = await fsp.readFile(manifestPath, 'utf-8');
|
|
590
994
|
const m = JSON.parse(raw);
|
|
591
|
-
const hasVideo = await fsp
|
|
592
|
-
|
|
995
|
+
const hasVideo = await fsp
|
|
996
|
+
.access(path.join(dir, e.name, 'session.webm'))
|
|
997
|
+
.then(() => true)
|
|
998
|
+
.catch(() => false);
|
|
999
|
+
const hasViewer = await fsp
|
|
1000
|
+
.access(path.join(dir, e.name, 'viewer.html'))
|
|
1001
|
+
.then(() => true)
|
|
1002
|
+
.catch(() => false);
|
|
593
1003
|
sessions.push({
|
|
594
1004
|
sessionId: m.sessionId,
|
|
595
1005
|
label: m.label,
|
|
@@ -611,16 +1021,20 @@ async function handleSessionsList() {
|
|
|
611
1021
|
sessions.sort((a, b) => (b.startedAt ?? '').localeCompare(a.startedAt ?? ''));
|
|
612
1022
|
// Also write an index.html that links to every viewer for quick browsing.
|
|
613
1023
|
try {
|
|
614
|
-
const items = sessions
|
|
1024
|
+
const items = sessions
|
|
1025
|
+
.map((s) => {
|
|
615
1026
|
const viewerLink = s.hasViewer ? `${s.sessionId}/viewer.html` : '#';
|
|
616
1027
|
const errorsTotal = s.errors ? s.errors.console + s.errors.network + s.errors.server : 0;
|
|
617
1028
|
const errBadge = errorsTotal > 0
|
|
618
1029
|
? `<span style="color:#c00;font-weight:600">${errorsTotal} err</span>`
|
|
619
1030
|
: '<span style="color:#0a0">clean</span>';
|
|
620
|
-
const labelHtml = s.label
|
|
1031
|
+
const labelHtml = s.label
|
|
1032
|
+
? `<span style="color:#06c">${escapeHtml(s.label)}</span> · `
|
|
1033
|
+
: '';
|
|
621
1034
|
const dur = s.duration ? `${s.duration.toFixed(1)}s` : '—';
|
|
622
|
-
return `<li><a href="${viewerLink}">${labelHtml}<code>${
|
|
623
|
-
})
|
|
1035
|
+
return `<li><a href="${viewerLink}">${labelHtml}<code>${escapeHtml(s.sessionId)}</code></a> · ${escapeHtml(s.url ?? '')} · ${dur} · ${s.actionCount} actions · ${errBadge}</li>`;
|
|
1036
|
+
})
|
|
1037
|
+
.join('\n');
|
|
624
1038
|
const indexHtml = `<!DOCTYPE html>
|
|
625
1039
|
<html><head><title>Sweetlink Sessions</title>
|
|
626
1040
|
<style>
|
|
@@ -635,15 +1049,21 @@ a{text-decoration:none;color:#222}
|
|
|
635
1049
|
</body></html>`;
|
|
636
1050
|
await fsp.writeFile(path.join(dir, 'index.html'), indexHtml);
|
|
637
1051
|
}
|
|
638
|
-
catch {
|
|
1052
|
+
catch {
|
|
1053
|
+
/* index is best-effort */
|
|
1054
|
+
}
|
|
639
1055
|
return { ok: true, data: { sessions, indexPath: path.join(dir, 'index.html') } };
|
|
640
1056
|
}
|
|
641
1057
|
catch (err) {
|
|
642
1058
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
643
1059
|
}
|
|
644
1060
|
}
|
|
645
|
-
function
|
|
646
|
-
return String(s)
|
|
1061
|
+
function escapeHtml(s) {
|
|
1062
|
+
return String(s)
|
|
1063
|
+
.replace(/&/g, '&')
|
|
1064
|
+
.replace(/</g, '<')
|
|
1065
|
+
.replace(/>/g, '>')
|
|
1066
|
+
.replace(/"/g, '"');
|
|
647
1067
|
}
|
|
648
1068
|
async function handleGenerateViewer(params) {
|
|
649
1069
|
const sessionDir = params.sessionDir;
|
|
@@ -663,7 +1083,10 @@ async function handleGenerateViewer(params) {
|
|
|
663
1083
|
return { ok: true, data: { viewerPath } };
|
|
664
1084
|
}
|
|
665
1085
|
catch (error) {
|
|
666
|
-
return {
|
|
1086
|
+
return {
|
|
1087
|
+
ok: false,
|
|
1088
|
+
error: `Failed to generate viewer: ${error instanceof Error ? error.message : error}`,
|
|
1089
|
+
};
|
|
667
1090
|
}
|
|
668
1091
|
}
|
|
669
1092
|
async function handleVisualDiff(params) {
|
|
@@ -692,55 +1115,36 @@ async function handleVisualDiff(params) {
|
|
|
692
1115
|
// ============================================================================
|
|
693
1116
|
// Request Handling
|
|
694
1117
|
// ============================================================================
|
|
1118
|
+
const DAEMON_HANDLERS = {
|
|
1119
|
+
ping: () => handlePing(),
|
|
1120
|
+
shutdown: () => handleShutdown(),
|
|
1121
|
+
screenshot: (params, url) => handleScreenshot(params, url),
|
|
1122
|
+
'screenshot-responsive': (params, url) => handleResponsiveScreenshot(params, url),
|
|
1123
|
+
snapshot: (params, url) => handleSnapshot(params, url),
|
|
1124
|
+
inspect: (params, url) => handleInspect(params, url),
|
|
1125
|
+
'click-ref': (params, url) => handleClickRef(params, url),
|
|
1126
|
+
'click-css': (params, url) => handleClickCss(params, url),
|
|
1127
|
+
'fill-ref': (params, url) => handleFillRef(params, url),
|
|
1128
|
+
'hover-ref': (params, url) => handleHoverRef(params, url),
|
|
1129
|
+
'press-key': (params, url) => handlePressKey(params, url),
|
|
1130
|
+
'console-read': (params, url) => handleConsoleRead(params, url),
|
|
1131
|
+
'network-read': (params, url) => handleNetworkRead(params, url),
|
|
1132
|
+
'dialog-read': (_params, url) => handleDialogRead(url),
|
|
1133
|
+
'screenshot-devices': (params, url) => handleScreenshotDevices(params, url),
|
|
1134
|
+
'visual-diff': (params) => handleVisualDiff(params),
|
|
1135
|
+
'record-start': (params, url) => handleRecordStart(params, url),
|
|
1136
|
+
'record-pause': () => handleRecordPause(),
|
|
1137
|
+
'record-resume': () => handleRecordResume(),
|
|
1138
|
+
'sessions-list': () => handleSessionsList(),
|
|
1139
|
+
'record-stop': () => handleRecordStop(),
|
|
1140
|
+
'record-status': () => handleRecordStatus(),
|
|
1141
|
+
'generate-viewer': (params) => handleGenerateViewer(params),
|
|
1142
|
+
};
|
|
695
1143
|
async function handleRequest(action, params, url) {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
return handleShutdown();
|
|
701
|
-
case 'screenshot':
|
|
702
|
-
return handleScreenshot(params, url);
|
|
703
|
-
case 'screenshot-responsive':
|
|
704
|
-
return handleResponsiveScreenshot(params, url);
|
|
705
|
-
case 'snapshot':
|
|
706
|
-
return handleSnapshot(params, url);
|
|
707
|
-
case 'click-ref':
|
|
708
|
-
return handleClickRef(params, url);
|
|
709
|
-
case 'click-css':
|
|
710
|
-
return handleClickCss(params, url);
|
|
711
|
-
case 'fill-ref':
|
|
712
|
-
return handleFillRef(params, url);
|
|
713
|
-
case 'hover-ref':
|
|
714
|
-
return handleHoverRef(params, url);
|
|
715
|
-
case 'press-key':
|
|
716
|
-
return handlePressKey(params, url);
|
|
717
|
-
case 'console-read':
|
|
718
|
-
return handleConsoleRead(params, url);
|
|
719
|
-
case 'network-read':
|
|
720
|
-
return handleNetworkRead(params, url);
|
|
721
|
-
case 'dialog-read':
|
|
722
|
-
return handleDialogRead(url);
|
|
723
|
-
case 'screenshot-devices':
|
|
724
|
-
return handleScreenshotDevices(params, url);
|
|
725
|
-
case 'visual-diff':
|
|
726
|
-
return handleVisualDiff(params);
|
|
727
|
-
case 'record-start':
|
|
728
|
-
return handleRecordStart(params, url);
|
|
729
|
-
case 'record-pause':
|
|
730
|
-
return handleRecordPause();
|
|
731
|
-
case 'record-resume':
|
|
732
|
-
return handleRecordResume();
|
|
733
|
-
case 'sessions-list':
|
|
734
|
-
return handleSessionsList();
|
|
735
|
-
case 'record-stop':
|
|
736
|
-
return handleRecordStop();
|
|
737
|
-
case 'record-status':
|
|
738
|
-
return handleRecordStatus();
|
|
739
|
-
case 'generate-viewer':
|
|
740
|
-
return handleGenerateViewer(params);
|
|
741
|
-
default:
|
|
742
|
-
return { ok: false, error: `Unknown action: ${action}` };
|
|
743
|
-
}
|
|
1144
|
+
const handler = DAEMON_HANDLERS[action];
|
|
1145
|
+
if (!handler)
|
|
1146
|
+
return { ok: false, error: `Unknown action: ${action}` };
|
|
1147
|
+
return handler(params, url);
|
|
744
1148
|
}
|
|
745
1149
|
function readBody(req) {
|
|
746
1150
|
return new Promise((resolve, reject) => {
|
|
@@ -807,8 +1211,8 @@ export function startServer(options) {
|
|
|
807
1211
|
const { promises: fsp } = await import('fs');
|
|
808
1212
|
const entries = await fsp.readdir('.sweetlink', { withFileTypes: true });
|
|
809
1213
|
const sessions = entries
|
|
810
|
-
.filter(e => e.isDirectory() && e.name.startsWith('session-'))
|
|
811
|
-
.map(e => e.name)
|
|
1214
|
+
.filter((e) => e.isDirectory() && e.name.startsWith('session-'))
|
|
1215
|
+
.map((e) => e.name)
|
|
812
1216
|
.sort()
|
|
813
1217
|
.reverse();
|
|
814
1218
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|