@ytspar/sweetlink 1.18.0 → 1.20.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.
Files changed (78) hide show
  1. package/README.md +40 -0
  2. package/dist/cli/outputSchemas.d.ts +57 -1
  3. package/dist/cli/outputSchemas.d.ts.map +1 -1
  4. package/dist/cli/outputSchemas.js +36 -1
  5. package/dist/cli/outputSchemas.js.map +1 -1
  6. package/dist/cli/sweetlink.js +537 -36
  7. package/dist/cli/sweetlink.js.map +1 -1
  8. package/dist/daemon/browser.d.ts.map +1 -1
  9. package/dist/daemon/browser.js.map +1 -1
  10. package/dist/daemon/client.d.ts +7 -0
  11. package/dist/daemon/client.d.ts.map +1 -1
  12. package/dist/daemon/client.js +16 -2
  13. package/dist/daemon/client.js.map +1 -1
  14. package/dist/daemon/demo.d.ts.map +1 -1
  15. package/dist/daemon/demo.js +6 -2
  16. package/dist/daemon/demo.js.map +1 -1
  17. package/dist/daemon/diff.d.ts.map +1 -1
  18. package/dist/daemon/diff.js +5 -3
  19. package/dist/daemon/diff.js.map +1 -1
  20. package/dist/daemon/evidence.d.ts.map +1 -1
  21. package/dist/daemon/evidence.js +5 -5
  22. package/dist/daemon/evidence.js.map +1 -1
  23. package/dist/daemon/index.js +1 -1
  24. package/dist/daemon/index.js.map +1 -1
  25. package/dist/daemon/listeners.d.ts.map +1 -1
  26. package/dist/daemon/listeners.js +7 -5
  27. package/dist/daemon/listeners.js.map +1 -1
  28. package/dist/daemon/recording.d.ts +5 -0
  29. package/dist/daemon/recording.d.ts.map +1 -1
  30. package/dist/daemon/recording.js +34 -11
  31. package/dist/daemon/recording.js.map +1 -1
  32. package/dist/daemon/refs.d.ts.map +1 -1
  33. package/dist/daemon/refs.js.map +1 -1
  34. package/dist/daemon/server.d.ts.map +1 -1
  35. package/dist/daemon/server.js +419 -80
  36. package/dist/daemon/server.js.map +1 -1
  37. package/dist/daemon/summary.d.ts +1 -1
  38. package/dist/daemon/summary.d.ts.map +1 -1
  39. package/dist/daemon/types.d.ts +1 -1
  40. package/dist/daemon/types.d.ts.map +1 -1
  41. package/dist/daemon/types.js.map +1 -1
  42. package/dist/daemon/viewer.d.ts +1 -1
  43. package/dist/daemon/viewer.d.ts.map +1 -1
  44. package/dist/daemon/viewer.js +18 -10
  45. package/dist/daemon/viewer.js.map +1 -1
  46. package/dist/ruler.js +3 -1
  47. package/dist/ruler.js.map +1 -1
  48. package/dist/runs.d.ts +34 -0
  49. package/dist/runs.d.ts.map +1 -0
  50. package/dist/runs.js +61 -0
  51. package/dist/runs.js.map +1 -0
  52. package/dist/server/index.d.ts.map +1 -1
  53. package/dist/server/index.js +20 -10
  54. package/dist/server/index.js.map +1 -1
  55. package/dist/simulator/android.d.ts +35 -0
  56. package/dist/simulator/android.d.ts.map +1 -0
  57. package/dist/simulator/android.js +127 -0
  58. package/dist/simulator/android.js.map +1 -0
  59. package/dist/simulator/ios.d.ts +39 -0
  60. package/dist/simulator/ios.d.ts.map +1 -0
  61. package/dist/simulator/ios.js +123 -0
  62. package/dist/simulator/ios.js.map +1 -0
  63. package/dist/term/ansi.d.ts +37 -0
  64. package/dist/term/ansi.d.ts.map +1 -0
  65. package/dist/term/ansi.js +205 -0
  66. package/dist/term/ansi.js.map +1 -0
  67. package/dist/term/player.d.ts +25 -0
  68. package/dist/term/player.d.ts.map +1 -0
  69. package/dist/term/player.js +243 -0
  70. package/dist/term/player.js.map +1 -0
  71. package/dist/term/recorder.d.ts +33 -0
  72. package/dist/term/recorder.d.ts.map +1 -0
  73. package/dist/term/recorder.js +77 -0
  74. package/dist/term/recorder.js.map +1 -0
  75. package/dist/vite.d.ts.map +1 -1
  76. package/dist/vite.js +8 -4
  77. package/dist/vite.js.map +1 -1
  78. package/package.json +1 -1
@@ -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 { consoleBuffer, dialogBuffer, formatConsoleEntries, formatNetworkEntries, getErrorCount, getWarningCount, networkBuffer, } from './listeners.js';
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.replace(/[^a-z0-9]/gi, '-').slice(0, 40).toLowerCase();
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,307 @@ async function captureFailure(page, reason) {
207
210
  return undefined;
208
211
  }
209
212
  }
213
+ function slugifyArtifact(value) {
214
+ return (value
215
+ .toLowerCase()
216
+ .replace(/[^a-z0-9]+/g, '-')
217
+ .replace(/^-|-$/g, '')
218
+ .slice(0, 72) || 'inspect');
219
+ }
220
+ function normalizeActionTranscript(value) {
221
+ if (!Array.isArray(value))
222
+ return [];
223
+ return value.flatMap((entry) => {
224
+ if (!entry || typeof entry !== 'object')
225
+ return [];
226
+ const record = entry;
227
+ const action = typeof record.action === 'string' ? record.action : undefined;
228
+ if (!action)
229
+ return [];
230
+ return [
231
+ {
232
+ action,
233
+ target: typeof record.target === 'string' ? record.target : undefined,
234
+ result: typeof record.result === 'string' ? record.result : undefined,
235
+ },
236
+ ];
237
+ });
238
+ }
239
+ async function getPageInfo(page) {
240
+ return page.evaluate(() => {
241
+ const nav = performance.getEntriesByType('navigation')[0];
242
+ const fcp = performance.getEntriesByName('first-contentful-paint')[0];
243
+ return {
244
+ url: location.href,
245
+ title: document.title,
246
+ viewport: {
247
+ width: window.innerWidth,
248
+ height: window.innerHeight,
249
+ deviceScaleFactor: window.devicePixelRatio,
250
+ },
251
+ vitals: {
252
+ fcp: fcp ? Math.round(fcp.startTime) : null,
253
+ pageSize: nav ? nav.transferSize || nav.encodedBodySize || null : null,
254
+ },
255
+ };
256
+ });
257
+ }
258
+ async function getAxeSource() {
259
+ try {
260
+ const axeModule = await import('axe-core');
261
+ const candidate = axeModule;
262
+ return candidate.source ?? candidate.default?.source ?? null;
263
+ }
264
+ catch {
265
+ return null;
266
+ }
267
+ }
268
+ async function runInspectA11y(page) {
269
+ const source = await getAxeSource();
270
+ if (!source) {
271
+ return {
272
+ ok: false,
273
+ error: 'axe-core is not available in this environment',
274
+ };
275
+ }
276
+ try {
277
+ await page.addScriptTag({ content: source });
278
+ return await page.evaluate(async () => {
279
+ const axe = window.axe;
280
+ if (!axe)
281
+ throw new Error('axe-core did not initialize on the page');
282
+ const compactIssue = (issue) => ({
283
+ id: issue.id,
284
+ impact: issue.impact ?? 'unknown',
285
+ help: issue.help ?? '',
286
+ description: issue.description ?? '',
287
+ helpUrl: issue.helpUrl ?? '',
288
+ nodes: (issue.nodes ?? []).slice(0, 3).map((node) => ({
289
+ target: (node.target ?? []).join(' '),
290
+ html: (node.html ?? '').slice(0, 180),
291
+ summary: node.failureSummary ??
292
+ node.any?.[0]?.message ??
293
+ node.all?.[0]?.message ??
294
+ node.none?.[0]?.message ??
295
+ '',
296
+ })),
297
+ });
298
+ const result = await axe.run(document, {
299
+ runOnly: {
300
+ type: 'tag',
301
+ values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'],
302
+ },
303
+ rules: {
304
+ 'color-contrast': { enabled: true },
305
+ },
306
+ });
307
+ const byImpact = {};
308
+ for (const violation of result.violations) {
309
+ const impact = violation.impact ?? 'unknown';
310
+ byImpact[impact] = (byImpact[impact] ?? 0) + 1;
311
+ }
312
+ return {
313
+ ok: true,
314
+ summary: {
315
+ violations: result.violations.length,
316
+ incomplete: result.incomplete.length,
317
+ passes: result.passes.length,
318
+ byImpact,
319
+ },
320
+ violations: result.violations.map(compactIssue),
321
+ incomplete: result.incomplete.map(compactIssue),
322
+ };
323
+ });
324
+ }
325
+ catch (error) {
326
+ return {
327
+ ok: false,
328
+ error: error instanceof Error ? error.message : String(error),
329
+ };
330
+ }
331
+ }
332
+ function inspectNextActions(counts, artifacts) {
333
+ const actions = [
334
+ `Open ${artifacts.summaryMarkdown} and ${artifacts.screenshotPng} before making visual claims.`,
335
+ ];
336
+ if (counts.consoleErrors > 0) {
337
+ actions.push('Investigate console errors before changing UI behavior.');
338
+ }
339
+ else if (counts.consoleWarnings > 0) {
340
+ actions.push('Review console warnings for stale props, hydration, or deprecated API signals.');
341
+ }
342
+ if (counts.networkFailures > 0) {
343
+ actions.push('Inspect failed network entries before assuming the UI state is a component bug.');
344
+ }
345
+ if ((counts.a11yViolations ?? 0) > 0 || (counts.a11yIncomplete ?? 0) > 0) {
346
+ actions.push(`Use ${artifacts.a11yJson ?? 'the a11y artifact'} to prioritize accessibility fixes.`);
347
+ }
348
+ if (counts.refs > 0) {
349
+ actions.push('Use @e refs for click/fill/press actions, then rerun inspect after DOM changes.');
350
+ }
351
+ else {
352
+ actions.push('No interactive refs were found; inspect the DOM/outline before attempting actions.');
353
+ }
354
+ return actions;
355
+ }
356
+ function renderInspectSummary(data) {
357
+ const transcript = data.actionTranscript.length > 0
358
+ ? data.actionTranscript
359
+ .map((entry, index) => {
360
+ const target = entry.target ? ` target=${entry.target}` : '';
361
+ const result = entry.result ? ` result=${entry.result}` : '';
362
+ return `${index + 1}. ${entry.action}${target}${result}`;
363
+ })
364
+ .join('\n')
365
+ : '(none supplied)';
366
+ const refs = data.refs.length > 0
367
+ ? data.refs.map((ref) => `- ${ref.ref} [${ref.role}] "${ref.name}"`).join('\n')
368
+ : '(no interactive refs)';
369
+ const a11ySummary = data.a11y
370
+ ? JSON.stringify(data.a11y.summary ?? { ok: data.a11y.ok, error: data.a11y.error }, null, 2)
371
+ : '(skipped)';
372
+ return [
373
+ '# Sweetlink Inspect',
374
+ '',
375
+ `- URL: ${data.url}`,
376
+ `- Title: ${data.title || '(untitled)'}`,
377
+ `- Generated: ${data.generatedAt}`,
378
+ `- Viewport: ${data.viewport.width}x${data.viewport.height} @${data.viewport.deviceScaleFactor}x`,
379
+ `- FCP: ${data.vitals.fcp ?? 'n/a'}ms`,
380
+ `- Page size: ${data.vitals.pageSize ?? 'n/a'} bytes`,
381
+ '',
382
+ '## Expected Outcome',
383
+ '',
384
+ data.expectedOutcome ?? '(not supplied)',
385
+ '',
386
+ '## Action Transcript',
387
+ '',
388
+ transcript,
389
+ '',
390
+ '## Counts',
391
+ '',
392
+ `- Interactive refs: ${data.counts.refs}`,
393
+ `- Console: ${data.counts.consoleEntries} entries, ${data.counts.consoleErrors} errors, ${data.counts.consoleWarnings} warnings`,
394
+ `- Network: ${data.counts.networkEntries} entries, ${data.counts.networkFailures} failures`,
395
+ `- Accessibility: ${data.counts.a11yViolations ?? 'n/a'} violations, ${data.counts.a11yIncomplete ?? 'n/a'} incomplete`,
396
+ '',
397
+ '## Next Actions',
398
+ '',
399
+ data.nextActions.map((action) => `- ${action}`).join('\n'),
400
+ '',
401
+ '## Artifacts',
402
+ '',
403
+ ...Object.entries(data.artifacts).map(([key, value]) => `- ${key}: ${value ?? '(not generated)'}`),
404
+ '',
405
+ '## Interactive Refs',
406
+ '',
407
+ refs,
408
+ '',
409
+ '## Console',
410
+ '',
411
+ data.consoleText,
412
+ '',
413
+ '## Network',
414
+ '',
415
+ data.networkText,
416
+ '',
417
+ '## Accessibility',
418
+ '',
419
+ a11ySummary,
420
+ '',
421
+ ].join('\n');
422
+ }
423
+ async function handleInspect(params, url) {
424
+ await initBrowser(url);
425
+ const page = getRecordingPage() ?? getPage();
426
+ const pageInfo = await getPageInfo(page);
427
+ const generatedAt = new Date().toISOString();
428
+ const label = typeof params.label === 'string' ? params.label : 'inspect';
429
+ const stamp = generatedAt.replace(/[:.]/g, '-');
430
+ const { promises: fsp } = await import('fs');
431
+ const path = await import('path');
432
+ const dir = path.resolve(`.sweetlink/inspect/${stamp}-${slugifyArtifact(label)}`);
433
+ await fsp.mkdir(dir, { recursive: true });
434
+ const lastRaw = typeof params.last === 'number' ? params.last : Number(params.last);
435
+ const last = Number.isFinite(lastRaw) && lastRaw > 0 ? Math.min(Math.floor(lastRaw), 500) : 50;
436
+ const includeA11y = params.includeA11y !== false;
437
+ const expectedOutcome = typeof params.expectedOutcome === 'string' ? params.expectedOutcome : undefined;
438
+ const actionTranscript = normalizeActionTranscript(params.actionTranscript);
439
+ const refMap = await buildRefMap(page, { interactive: true });
440
+ const snapshotText = formatRefMap(refMap);
441
+ const screenshotBuffer = await page.screenshot({ fullPage: true });
442
+ const consoleEntries = consoleBuffer.toArray().slice(-last);
443
+ const networkEntries = networkBuffer.toArray().slice(-last);
444
+ const consoleText = formatConsoleEntries(consoleEntries);
445
+ const networkText = formatNetworkEntries(networkEntries);
446
+ const a11y = includeA11y ? await runInspectA11y(page) : undefined;
447
+ const artifacts = {
448
+ dir,
449
+ summaryMarkdown: path.join(dir, 'SUMMARY.md'),
450
+ contextJson: path.join(dir, 'context.json'),
451
+ screenshotPng: path.join(dir, 'screenshot.png'),
452
+ snapshotMarkdown: path.join(dir, 'snapshot.md'),
453
+ consoleText: path.join(dir, 'console.txt'),
454
+ networkText: path.join(dir, 'network.txt'),
455
+ a11yJson: includeA11y ? path.join(dir, 'a11y.json') : undefined,
456
+ };
457
+ const a11ySummary = a11y?.summary;
458
+ const counts = {
459
+ refs: refMap.entries.length,
460
+ consoleEntries: consoleEntries.length,
461
+ consoleErrors: consoleEntries.filter((entry) => entry.level === 'error').length,
462
+ consoleWarnings: consoleEntries.filter((entry) => entry.level === 'warning').length,
463
+ networkEntries: networkEntries.length,
464
+ networkFailures: networkEntries.filter((entry) => entry.status >= 400 || entry.status === 0)
465
+ .length,
466
+ a11yViolations: a11ySummary?.violations,
467
+ a11yIncomplete: a11ySummary?.incomplete,
468
+ };
469
+ const nextActions = inspectNextActions(counts, artifacts);
470
+ const context = {
471
+ url: pageInfo.url,
472
+ title: pageInfo.title,
473
+ generatedAt,
474
+ viewport: pageInfo.viewport,
475
+ vitals: pageInfo.vitals,
476
+ artifacts,
477
+ counts,
478
+ refs: refMap.entries,
479
+ console: { entries: consoleEntries, formatted: consoleText },
480
+ network: { entries: networkEntries, formatted: networkText },
481
+ a11y,
482
+ expectedOutcome,
483
+ actionTranscript,
484
+ failureArtifacts: [],
485
+ nextActions,
486
+ };
487
+ await fsp.writeFile(artifacts.screenshotPng, screenshotBuffer);
488
+ await fsp.writeFile(artifacts.snapshotMarkdown, snapshotText, 'utf-8');
489
+ await fsp.writeFile(artifacts.consoleText, consoleText, 'utf-8');
490
+ await fsp.writeFile(artifacts.networkText, networkText, 'utf-8');
491
+ if (artifacts.a11yJson) {
492
+ await fsp.writeFile(artifacts.a11yJson, JSON.stringify(a11y ?? null, null, 2), 'utf-8');
493
+ }
494
+ const summary = renderInspectSummary({
495
+ url: context.url,
496
+ title: context.title,
497
+ generatedAt: context.generatedAt,
498
+ viewport: context.viewport,
499
+ vitals: context.vitals,
500
+ artifacts: context.artifacts,
501
+ counts: context.counts,
502
+ refs: context.refs,
503
+ consoleText,
504
+ networkText,
505
+ a11y,
506
+ expectedOutcome,
507
+ actionTranscript,
508
+ nextActions,
509
+ });
510
+ await fsp.writeFile(artifacts.summaryMarkdown, summary, 'utf-8');
511
+ await fsp.writeFile(artifacts.contextJson, JSON.stringify(context, null, 2), 'utf-8');
512
+ return { ok: true, data: context };
513
+ }
210
514
  async function handleClickRef(params, url) {
211
515
  await initBrowser(url);
212
516
  // Use recording page if recording, otherwise main page
@@ -238,7 +542,17 @@ async function handleClickRef(params, url) {
238
542
  }
239
543
  const box = await locator.boundingBox();
240
544
  const t0 = Date.now();
241
- await locator.click();
545
+ try {
546
+ await locator.click();
547
+ }
548
+ catch (error) {
549
+ const failureScreenshot = await captureFailure(page, `click-failed-${ref}`);
550
+ return {
551
+ ok: false,
552
+ error: `Click failed for ${ref}: ${error instanceof Error ? error.message : String(error)}`,
553
+ data: failureScreenshot ? { failureScreenshot } : undefined,
554
+ };
555
+ }
242
556
  const durationMs = Date.now() - t0;
243
557
  // Log action if recording
244
558
  if (isRecording()) {
@@ -277,7 +591,17 @@ async function handleFillRef(params, url) {
277
591
  }
278
592
  const box = await locator.boundingBox();
279
593
  const t0 = Date.now();
280
- await locator.fill(value);
594
+ try {
595
+ await locator.fill(value);
596
+ }
597
+ catch (error) {
598
+ const failureScreenshot = await captureFailure(page, `fill-failed-${ref}`);
599
+ return {
600
+ ok: false,
601
+ error: `Fill failed for ${ref}: ${error instanceof Error ? error.message : String(error)}`,
602
+ data: failureScreenshot ? { failureScreenshot } : undefined,
603
+ };
604
+ }
281
605
  const durationMs = Date.now() - t0;
282
606
  if (isRecording()) {
283
607
  await logAction('fill', [ref, value], page, box ?? undefined, durationMs);
@@ -334,11 +658,13 @@ async function handleClickCss(params, url) {
334
658
  return { ours: true };
335
659
  if (top === intended || intended.contains(top))
336
660
  return { ours: true };
337
- const desc = top.tagName.toLowerCase() +
338
- (top.id ? '#' + top.id : '') +
339
- (top.className && typeof top.className === 'string'
340
- ? '.' + top.className.split(/\s+/).filter(Boolean).slice(0, 2).join('.')
341
- : '');
661
+ const desc = [
662
+ top.tagName.toLowerCase(),
663
+ top.id ? `#${top.id}` : '',
664
+ top.className && typeof top.className === 'string'
665
+ ? `.${top.className.split(/\s+/).filter(Boolean).slice(0, 2).join('.')}`
666
+ : '',
667
+ ].join('');
342
668
  return { ours: false, coveredBy: desc };
343
669
  }, { cx, cy, sel: selector ?? null });
344
670
  if (!info.ours) {
@@ -499,11 +825,15 @@ async function handleRecordStart(params, url) {
499
825
  const storageState = params.storageState || undefined;
500
826
  const trace = params.trace || undefined;
501
827
  const result = await startRecording(browser, url, '.sweetlink', {
502
- viewport, label, storageState, trace,
828
+ viewport,
829
+ label,
830
+ storageState,
831
+ trace,
503
832
  });
504
833
  return { ok: true, data: { sessionId: result.sessionId, label, trace: !!trace } };
505
834
  }
506
835
  async function handleRecordStop() {
836
+ const eventCursors = getRecordingEventCursors();
507
837
  const manifest = await stopRecording();
508
838
  if (!manifest) {
509
839
  return { ok: false, error: 'No recording in progress' };
@@ -513,8 +843,14 @@ async function handleRecordStop() {
513
843
  let viewerPath;
514
844
  let summaryPath;
515
845
  try {
516
- const consoleLogs = consoleBuffer.toArray();
517
- const networkLogs = networkBuffer.toArray();
846
+ const allConsoleLogs = consoleBuffer.toArray();
847
+ const allNetworkLogs = networkBuffer.toArray();
848
+ const consoleLogs = eventCursors
849
+ ? allConsoleLogs.slice(eventCursors.consoleStartCursor)
850
+ : allConsoleLogs;
851
+ const networkLogs = eventCursors
852
+ ? allNetworkLogs.slice(eventCursors.networkStartCursor)
853
+ : allNetworkLogs;
518
854
  viewerPath = await generateViewer(manifest, {
519
855
  sessionDir,
520
856
  consoleEntries: consoleLogs,
@@ -523,16 +859,17 @@ async function handleRecordStop() {
523
859
  // Generate SUMMARY.md
524
860
  const { promises: fsp } = await import('fs');
525
861
  // Detect server errors from console log messages
526
- const consoleText = consoleLogs.map(e => e.message).join('\n');
862
+ const consoleText = consoleLogs.map((e) => e.message).join('\n');
527
863
  const serverErrors = detectServerErrors(consoleText);
528
864
  if (serverErrors.length > 0) {
529
865
  manifest.errors.server = serverErrors.length;
530
866
  }
867
+ await fsp.writeFile(`${sessionDir}/sweetlink-session.json`, JSON.stringify(manifest, null, 2), 'utf-8');
531
868
  const summaryMd = generateSummary({
532
869
  manifest,
533
870
  consoleEntries: consoleLogs,
534
871
  networkEntries: networkLogs,
535
- serverErrors: serverErrors.map(e => ({
872
+ serverErrors: serverErrors.map((e) => ({
536
873
  source: 'server',
537
874
  message: e.line,
538
875
  timestamp: Date.now(),
@@ -549,7 +886,9 @@ async function handleRecordStop() {
549
886
  console.error('[Daemon] Report generation error:', e);
550
887
  }
551
888
  // Include a browser-accessible URL for the viewer
552
- const viewerUrl = manifest.sessionId && daemonPort ? `http://127.0.0.1:${daemonPort}/viewer/${manifest.sessionId}` : undefined;
889
+ const viewerUrl = manifest.sessionId && daemonPort
890
+ ? `http://127.0.0.1:${daemonPort}/viewer/${manifest.sessionId}`
891
+ : undefined;
553
892
  return { ok: true, data: { manifest, viewerPath, viewerUrl, summaryPath } };
554
893
  }
555
894
  async function handleRecordStatus() {
@@ -588,8 +927,14 @@ async function handleSessionsList() {
588
927
  try {
589
928
  const raw = await fsp.readFile(manifestPath, 'utf-8');
590
929
  const m = JSON.parse(raw);
591
- const hasVideo = await fsp.access(path.join(dir, e.name, 'session.webm')).then(() => true).catch(() => false);
592
- const hasViewer = await fsp.access(path.join(dir, e.name, 'viewer.html')).then(() => true).catch(() => false);
930
+ const hasVideo = await fsp
931
+ .access(path.join(dir, e.name, 'session.webm'))
932
+ .then(() => true)
933
+ .catch(() => false);
934
+ const hasViewer = await fsp
935
+ .access(path.join(dir, e.name, 'viewer.html'))
936
+ .then(() => true)
937
+ .catch(() => false);
593
938
  sessions.push({
594
939
  sessionId: m.sessionId,
595
940
  label: m.label,
@@ -611,16 +956,20 @@ async function handleSessionsList() {
611
956
  sessions.sort((a, b) => (b.startedAt ?? '').localeCompare(a.startedAt ?? ''));
612
957
  // Also write an index.html that links to every viewer for quick browsing.
613
958
  try {
614
- const items = sessions.map((s) => {
959
+ const items = sessions
960
+ .map((s) => {
615
961
  const viewerLink = s.hasViewer ? `${s.sessionId}/viewer.html` : '#';
616
962
  const errorsTotal = s.errors ? s.errors.console + s.errors.network + s.errors.server : 0;
617
963
  const errBadge = errorsTotal > 0
618
964
  ? `<span style="color:#c00;font-weight:600">${errorsTotal} err</span>`
619
965
  : '<span style="color:#0a0">clean</span>';
620
- const labelHtml = s.label ? `<span style="color:#06c">${escape(s.label)}</span> · ` : '';
966
+ const labelHtml = s.label
967
+ ? `<span style="color:#06c">${escapeHtml(s.label)}</span> · `
968
+ : '';
621
969
  const dur = s.duration ? `${s.duration.toFixed(1)}s` : '—';
622
- return `<li><a href="${viewerLink}">${labelHtml}<code>${escape(s.sessionId)}</code></a> · ${escape(s.url ?? '')} · ${dur} · ${s.actionCount} actions · ${errBadge}</li>`;
623
- }).join('\n');
970
+ return `<li><a href="${viewerLink}">${labelHtml}<code>${escapeHtml(s.sessionId)}</code></a> · ${escapeHtml(s.url ?? '')} · ${dur} · ${s.actionCount} actions · ${errBadge}</li>`;
971
+ })
972
+ .join('\n');
624
973
  const indexHtml = `<!DOCTYPE html>
625
974
  <html><head><title>Sweetlink Sessions</title>
626
975
  <style>
@@ -635,15 +984,21 @@ a{text-decoration:none;color:#222}
635
984
  </body></html>`;
636
985
  await fsp.writeFile(path.join(dir, 'index.html'), indexHtml);
637
986
  }
638
- catch { /* index is best-effort */ }
987
+ catch {
988
+ /* index is best-effort */
989
+ }
639
990
  return { ok: true, data: { sessions, indexPath: path.join(dir, 'index.html') } };
640
991
  }
641
992
  catch (err) {
642
993
  return { ok: false, error: err instanceof Error ? err.message : String(err) };
643
994
  }
644
995
  }
645
- function escape(s) {
646
- return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
996
+ function escapeHtml(s) {
997
+ return String(s)
998
+ .replace(/&/g, '&amp;')
999
+ .replace(/</g, '&lt;')
1000
+ .replace(/>/g, '&gt;')
1001
+ .replace(/"/g, '&quot;');
647
1002
  }
648
1003
  async function handleGenerateViewer(params) {
649
1004
  const sessionDir = params.sessionDir;
@@ -663,7 +1018,10 @@ async function handleGenerateViewer(params) {
663
1018
  return { ok: true, data: { viewerPath } };
664
1019
  }
665
1020
  catch (error) {
666
- return { ok: false, error: `Failed to generate viewer: ${error instanceof Error ? error.message : error}` };
1021
+ return {
1022
+ ok: false,
1023
+ error: `Failed to generate viewer: ${error instanceof Error ? error.message : error}`,
1024
+ };
667
1025
  }
668
1026
  }
669
1027
  async function handleVisualDiff(params) {
@@ -692,55 +1050,36 @@ async function handleVisualDiff(params) {
692
1050
  // ============================================================================
693
1051
  // Request Handling
694
1052
  // ============================================================================
1053
+ const DAEMON_HANDLERS = {
1054
+ ping: () => handlePing(),
1055
+ shutdown: () => handleShutdown(),
1056
+ screenshot: (params, url) => handleScreenshot(params, url),
1057
+ 'screenshot-responsive': (params, url) => handleResponsiveScreenshot(params, url),
1058
+ snapshot: (params, url) => handleSnapshot(params, url),
1059
+ inspect: (params, url) => handleInspect(params, url),
1060
+ 'click-ref': (params, url) => handleClickRef(params, url),
1061
+ 'click-css': (params, url) => handleClickCss(params, url),
1062
+ 'fill-ref': (params, url) => handleFillRef(params, url),
1063
+ 'hover-ref': (params, url) => handleHoverRef(params, url),
1064
+ 'press-key': (params, url) => handlePressKey(params, url),
1065
+ 'console-read': (params, url) => handleConsoleRead(params, url),
1066
+ 'network-read': (params, url) => handleNetworkRead(params, url),
1067
+ 'dialog-read': (_params, url) => handleDialogRead(url),
1068
+ 'screenshot-devices': (params, url) => handleScreenshotDevices(params, url),
1069
+ 'visual-diff': (params) => handleVisualDiff(params),
1070
+ 'record-start': (params, url) => handleRecordStart(params, url),
1071
+ 'record-pause': () => handleRecordPause(),
1072
+ 'record-resume': () => handleRecordResume(),
1073
+ 'sessions-list': () => handleSessionsList(),
1074
+ 'record-stop': () => handleRecordStop(),
1075
+ 'record-status': () => handleRecordStatus(),
1076
+ 'generate-viewer': (params) => handleGenerateViewer(params),
1077
+ };
695
1078
  async function handleRequest(action, params, url) {
696
- switch (action) {
697
- case 'ping':
698
- return handlePing();
699
- case 'shutdown':
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
- }
1079
+ const handler = DAEMON_HANDLERS[action];
1080
+ if (!handler)
1081
+ return { ok: false, error: `Unknown action: ${action}` };
1082
+ return handler(params, url);
744
1083
  }
745
1084
  function readBody(req) {
746
1085
  return new Promise((resolve, reject) => {
@@ -807,8 +1146,8 @@ export function startServer(options) {
807
1146
  const { promises: fsp } = await import('fs');
808
1147
  const entries = await fsp.readdir('.sweetlink', { withFileTypes: true });
809
1148
  const sessions = entries
810
- .filter(e => e.isDirectory() && e.name.startsWith('session-'))
811
- .map(e => e.name)
1149
+ .filter((e) => e.isDirectory() && e.name.startsWith('session-'))
1150
+ .map((e) => e.name)
812
1151
  .sort()
813
1152
  .reverse();
814
1153
  res.writeHead(200, { 'Content-Type': 'application/json' });