@ytspar/sweetlink 1.13.0 → 1.14.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 +91 -12
  2. package/claude-skills/screenshot/SKILL.md +121 -20
  3. package/dist/cli/outputSchemas.d.ts +16 -0
  4. package/dist/cli/outputSchemas.d.ts.map +1 -1
  5. package/dist/cli/outputSchemas.js +33 -0
  6. package/dist/cli/outputSchemas.js.map +1 -1
  7. package/dist/cli/sweetlink-dev.js +0 -0
  8. package/dist/cli/sweetlink.js +347 -11
  9. package/dist/cli/sweetlink.js.map +1 -1
  10. package/dist/daemon/browser.d.ts +51 -0
  11. package/dist/daemon/browser.d.ts.map +1 -0
  12. package/dist/daemon/browser.js +153 -0
  13. package/dist/daemon/browser.js.map +1 -0
  14. package/dist/daemon/client.d.ts +32 -0
  15. package/dist/daemon/client.d.ts.map +1 -0
  16. package/dist/daemon/client.js +133 -0
  17. package/dist/daemon/client.js.map +1 -0
  18. package/dist/daemon/cursor.d.ts +15 -0
  19. package/dist/daemon/cursor.d.ts.map +1 -0
  20. package/dist/daemon/cursor.js +76 -0
  21. package/dist/daemon/cursor.js.map +1 -0
  22. package/dist/daemon/devices.d.ts +39 -0
  23. package/dist/daemon/devices.d.ts.map +1 -0
  24. package/dist/daemon/devices.js +101 -0
  25. package/dist/daemon/devices.js.map +1 -0
  26. package/dist/daemon/diff.d.ts +20 -0
  27. package/dist/daemon/diff.d.ts.map +1 -0
  28. package/dist/daemon/diff.js +181 -0
  29. package/dist/daemon/diff.js.map +1 -0
  30. package/dist/daemon/evidence.d.ts +29 -0
  31. package/dist/daemon/evidence.d.ts.map +1 -0
  32. package/dist/daemon/evidence.js +130 -0
  33. package/dist/daemon/evidence.js.map +1 -0
  34. package/dist/daemon/index.d.ts +10 -0
  35. package/dist/daemon/index.d.ts.map +1 -0
  36. package/dist/daemon/index.js +90 -0
  37. package/dist/daemon/index.js.map +1 -0
  38. package/dist/daemon/listeners.d.ts +55 -0
  39. package/dist/daemon/listeners.d.ts.map +1 -0
  40. package/dist/daemon/listeners.js +129 -0
  41. package/dist/daemon/listeners.js.map +1 -0
  42. package/dist/daemon/recording.d.ts +44 -0
  43. package/dist/daemon/recording.d.ts.map +1 -0
  44. package/dist/daemon/recording.js +133 -0
  45. package/dist/daemon/recording.js.map +1 -0
  46. package/dist/daemon/refs.d.ts +70 -0
  47. package/dist/daemon/refs.d.ts.map +1 -0
  48. package/dist/daemon/refs.js +185 -0
  49. package/dist/daemon/refs.js.map +1 -0
  50. package/dist/daemon/ringBuffer.d.ts +26 -0
  51. package/dist/daemon/ringBuffer.d.ts.map +1 -0
  52. package/dist/daemon/ringBuffer.js +54 -0
  53. package/dist/daemon/ringBuffer.js.map +1 -0
  54. package/dist/daemon/server.d.ts +23 -0
  55. package/dist/daemon/server.d.ts.map +1 -0
  56. package/dist/daemon/server.js +508 -0
  57. package/dist/daemon/server.js.map +1 -0
  58. package/dist/daemon/session.d.ts +41 -0
  59. package/dist/daemon/session.d.ts.map +1 -0
  60. package/dist/daemon/session.js +8 -0
  61. package/dist/daemon/session.js.map +1 -0
  62. package/dist/daemon/stateFile.d.ts +49 -0
  63. package/dist/daemon/stateFile.d.ts.map +1 -0
  64. package/dist/daemon/stateFile.js +162 -0
  65. package/dist/daemon/stateFile.js.map +1 -0
  66. package/dist/daemon/types.d.ts +72 -0
  67. package/dist/daemon/types.d.ts.map +1 -0
  68. package/dist/daemon/types.js +28 -0
  69. package/dist/daemon/types.js.map +1 -0
  70. package/dist/daemon/viewer.d.ts +33 -0
  71. package/dist/daemon/viewer.d.ts.map +1 -0
  72. package/dist/daemon/viewer.js +226 -0
  73. package/dist/daemon/viewer.js.map +1 -0
  74. package/dist/daemon/visualDiff.d.ts +34 -0
  75. package/dist/daemon/visualDiff.d.ts.map +1 -0
  76. package/dist/daemon/visualDiff.js +80 -0
  77. package/dist/daemon/visualDiff.js.map +1 -0
  78. package/package.json +20 -12
@@ -13,6 +13,8 @@ import { screenshotViaPlaywright } from '../playwright.js';
13
13
  import { getCardHeaderPreset, getNavigationPreset, measureViaPlaywright } from '../ruler.js';
14
14
  import { DEFAULT_WS_PORT, MAX_PORT_RETRIES, WS_PORT_OFFSET } from '../types.js';
15
15
  import { SCREENSHOT_DIR } from '../urlUtils.js';
16
+ import { daemonRequest, ensureDaemon, getDaemonStatus, stopDaemon } from '../daemon/client.js';
17
+ import { uploadEvidence } from '../daemon/evidence.js';
16
18
  import { emitJson, printOutputSchema } from './outputSchemas.js';
17
19
  const COMMON_APP_PORTS = [3000, 3001, 4000, 5173, 5174, 8000, 8080];
18
20
  /**
@@ -421,6 +423,54 @@ async function screenshot(options) {
421
423
  process.exit(1);
422
424
  }
423
425
  }
426
+ // ── HiFi / Responsive path (persistent daemon) ──
427
+ if (options.hifi || options.responsive) {
428
+ console.log(`[Sweetlink] Taking ${options.responsive ? 'responsive' : 'hifi'} screenshot via daemon...`);
429
+ const daemonState = await ensureDaemon(findProjectRoot(), targetUrl);
430
+ if (options.responsive) {
431
+ const resp = await daemonRequest(daemonState, 'screenshot-responsive', {
432
+ fullPage: options.fullPage,
433
+ });
434
+ const data = resp.data;
435
+ const outputDir = options.output
436
+ ? path.dirname(options.output)
437
+ : path.join(findProjectRoot(), SCREENSHOT_DIR);
438
+ ensureDir(path.join(outputDir, 'placeholder'));
439
+ const paths = [];
440
+ for (const shot of data.screenshots) {
441
+ const filename = `responsive-${shot.label}-${Date.now()}.png`;
442
+ const outPath = path.join(outputDir, filename);
443
+ fs.writeFileSync(outPath, Buffer.from(shot.screenshot, 'base64'));
444
+ paths.push(outPath);
445
+ console.log(` ${shot.label} (${shot.width}x${shot.height}): ${getRelativePath(outPath)}`);
446
+ }
447
+ const first = data.screenshots[0];
448
+ return {
449
+ path: getRelativePath(paths[0]),
450
+ width: first.width,
451
+ height: first.height,
452
+ method: 'Daemon (responsive)',
453
+ };
454
+ }
455
+ // Single hifi screenshot
456
+ const resp = await daemonRequest(daemonState, 'screenshot', {
457
+ selector: options.selector,
458
+ fullPage: options.fullPage,
459
+ viewport: options.viewport,
460
+ });
461
+ const data = resp.data;
462
+ const outputPath = options.output || getDefaultScreenshotPath();
463
+ ensureDir(outputPath);
464
+ fs.writeFileSync(outputPath, Buffer.from(data.screenshot, 'base64'));
465
+ reportScreenshotSuccess(outputPath, data.width, data.height, 'Daemon (hifi)', options.selector);
466
+ return {
467
+ path: getRelativePath(outputPath),
468
+ width: data.width,
469
+ height: data.height,
470
+ method: 'Daemon (hifi)',
471
+ selector: options.selector,
472
+ };
473
+ }
424
474
  console.log('[Sweetlink] Taking screenshot...');
425
475
  // Warn if using /tmp/ instead of .tmp/ (project-relative path is preferred)
426
476
  if (options.output?.startsWith('/tmp/')) {
@@ -1497,6 +1547,8 @@ const COMMAND_HELP = {
1497
1547
  --viewport <preset|WxH> Viewport preset for Playwright (mobile, tablet, desktop) or WIDTHxHEIGHT
1498
1548
  --force-cdp Force Playwright/CDP method
1499
1549
  --force-ws Force WebSocket/html2canvas method (default)
1550
+ --hifi Pixel-perfect via persistent Playwright daemon (~150ms after startup)
1551
+ --responsive Screenshots at 3 breakpoints (375/768/1280px) via daemon
1500
1552
  --no-wait Skip server readiness check (use if server is already running)
1501
1553
  --wait-timeout <ms> Max time to wait for server (default: 30000ms)
1502
1554
 
@@ -1504,6 +1556,7 @@ const COMMAND_HELP = {
1504
1556
  Tier 1 (WS, viewport): ~50-300KB PNG at 0.5x scale
1505
1557
  Tier 1 (WS, --full-page): ~1-5MB PNG (entire page)
1506
1558
  Tier 2 (Playwright): ~200-800KB PNG at native resolution
1559
+ Tier 3 (--hifi): ~200-800KB PNG, persistent daemon, fastest repeat shots
1507
1560
 
1508
1561
  Examples:
1509
1562
  pnpm sweetlink screenshot # Viewport screenshot (small)
@@ -1511,7 +1564,9 @@ const COMMAND_HELP = {
1511
1564
  pnpm sweetlink screenshot --selector ".company-card" # Element screenshot
1512
1565
  pnpm sweetlink screenshot --full-page # Full scrollable page
1513
1566
  pnpm sweetlink screenshot --force-cdp --viewport tablet # Playwright at 768x1024
1514
- pnpm sweetlink screenshot --force-cdp --width 375 --height 667 # Playwright at iPhone SE`,
1567
+ pnpm sweetlink screenshot --force-cdp --width 375 --height 667 # Playwright at iPhone SE
1568
+ pnpm sweetlink screenshot --hifi # Pixel-perfect via daemon
1569
+ pnpm sweetlink screenshot --responsive # 3 breakpoints via daemon`,
1515
1570
  query: ` query --selector <css-selector> [options]
1516
1571
  Query DOM elements and return data
1517
1572
 
@@ -1717,6 +1772,77 @@ const COMMAND_HELP = {
1717
1772
 
1718
1773
  Examples:
1719
1774
  pnpm sweetlink setup`,
1775
+ daemon: ` daemon [start|stop|status] [options]
1776
+ Manage the persistent Playwright daemon process.
1777
+ The daemon auto-starts on first --hifi command and auto-stops after 30min idle.
1778
+
1779
+ Subcommands:
1780
+ start Start the daemon (if not already running)
1781
+ stop Stop the daemon
1782
+ status Show daemon status (default)
1783
+
1784
+ Options:
1785
+ --url <url> Dev server URL (default: http://localhost:3000)
1786
+
1787
+ Examples:
1788
+ pnpm sweetlink daemon # Show status
1789
+ pnpm sweetlink daemon start --url http://localhost:5173
1790
+ pnpm sweetlink daemon stop`,
1791
+ snapshot: ` snapshot [options]
1792
+ Capture accessibility tree snapshot with element refs (requires daemon).
1793
+
1794
+ Options:
1795
+ -i, --interactive Show only interactive elements with @e refs
1796
+ -D, --diff Diff against previous snapshot
1797
+ -a, --annotate Annotated screenshot with ref labels
1798
+ -o, --output <path> Output path for annotated screenshot
1799
+
1800
+ Examples:
1801
+ pnpm sweetlink snapshot -i # List interactive elements with @refs
1802
+ pnpm sweetlink snapshot -D # Diff against previous snapshot
1803
+ pnpm sweetlink snapshot -a -o /tmp/annotated.png`,
1804
+ console: ` console [options]
1805
+ Read console messages from daemon ring buffer (always-on capture).
1806
+ Replaces /console-check-sweetlink with better coverage.
1807
+
1808
+ Options:
1809
+ --errors Show only errors
1810
+ --last <n> Show only last N entries
1811
+ --url <url> Dev server URL (default: http://localhost:3000)
1812
+
1813
+ Examples:
1814
+ pnpm sweetlink console # All console messages
1815
+ pnpm sweetlink console --errors # Errors only
1816
+ pnpm sweetlink console --last 20 # Last 20 entries`,
1817
+ fill: ` fill <@ref> <value> [options]
1818
+ Fill an input element by @ref (requires daemon + snapshot).
1819
+
1820
+ Examples:
1821
+ pnpm sweetlink fill @e2 "test@example.com"`,
1822
+ proof: ` proof --pr <number> [options]
1823
+ Upload session evidence to a GitHub PR.
1824
+ Posts a formatted comment with action timeline and error summary.
1825
+
1826
+ Options:
1827
+ --pr <number> PR number (required)
1828
+ --session <dir> Session directory (default: .sweetlink)
1829
+ --repo <owner/repo> Repository (default: current repo)
1830
+
1831
+ Examples:
1832
+ pnpm sweetlink proof --pr 123`,
1833
+ record: ` record [start|stop|status]
1834
+ Record browser sessions with action timeline.
1835
+
1836
+ Subcommands:
1837
+ start Begin recording (captures screenshots at each action)
1838
+ stop Stop recording and generate session manifest
1839
+ status Show recording status (default)
1840
+
1841
+ Examples:
1842
+ pnpm sweetlink record start
1843
+ pnpm sweetlink snapshot -i
1844
+ pnpm sweetlink click @e3
1845
+ pnpm sweetlink record stop`,
1720
1846
  };
1721
1847
  // Aliases that map to canonical command names
1722
1848
  const COMMAND_ALIASES = {
@@ -1815,6 +1941,12 @@ if (hasFlag('--output-schema')) {
1815
1941
  'cleanup',
1816
1942
  'wait',
1817
1943
  'status',
1944
+ 'daemon',
1945
+ 'snapshot',
1946
+ 'fill',
1947
+ 'console',
1948
+ 'record',
1949
+ 'proof',
1818
1950
  ];
1819
1951
  const schemaCommand = knownCommands.includes(commandType)
1820
1952
  ? commandType === 'measure'
@@ -1932,6 +2064,8 @@ async function handleStatusCommand() {
1932
2064
  fullPage: hasFlag('--full-page'),
1933
2065
  forceCDP: hasFlag('--force-cdp'),
1934
2066
  forceWS: hasFlag('--force-ws'),
2067
+ hifi: hasFlag('--hifi'),
2068
+ responsive: hasFlag('--responsive'),
1935
2069
  a11y: hasFlag('--a11y'),
1936
2070
  viewport: getArg('--viewport'),
1937
2071
  width: getArg('--width') ? parseInt(getArg('--width'), 10) : undefined,
@@ -1999,18 +2133,49 @@ async function handleStatusCommand() {
1999
2133
  });
2000
2134
  break;
2001
2135
  }
2002
- case 'click':
2003
- result = await click({
2004
- selector: getArg('--selector'),
2005
- text: getArg('--text'),
2006
- index: getArg('--index') ? parseInt(getArg('--index'), 10) : undefined,
2007
- });
2136
+ case 'click': {
2137
+ const clickTarget = getArg('--selector') ?? args[1];
2138
+ // Route @e refs to daemon
2139
+ if (clickTarget && /^@e\d+$/.test(clickTarget)) {
2140
+ const projRoot = findProjectRoot();
2141
+ const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2142
+ const state = await ensureDaemon(projRoot, targetUrl);
2143
+ await daemonRequest(state, 'click-ref', { ref: clickTarget });
2144
+ console.log(`[Sweetlink] Clicked ${clickTarget}`);
2145
+ result = { clicked: clickTarget, found: 1, index: 0 };
2146
+ }
2147
+ else {
2148
+ result = await click({
2149
+ selector: clickTarget,
2150
+ text: getArg('--text'),
2151
+ index: getArg('--index') ? parseInt(getArg('--index'), 10) : undefined,
2152
+ });
2153
+ }
2008
2154
  break;
2009
- case 'network':
2010
- result = await getNetwork({
2011
- filter: getArg('--filter'),
2012
- });
2155
+ }
2156
+ case 'network': {
2157
+ // If --failed flag is present and daemon is running, use daemon ring buffer
2158
+ if (hasFlag('--failed')) {
2159
+ const projRoot = findProjectRoot();
2160
+ const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2161
+ const lastN = getArg('--last') ? parseInt(getArg('--last'), 10) : undefined;
2162
+ const state = await ensureDaemon(projRoot, targetUrl);
2163
+ const resp = await daemonRequest(state, 'network-read', {
2164
+ failed: true,
2165
+ last: lastN,
2166
+ });
2167
+ const data = resp.data;
2168
+ console.log(data.formatted);
2169
+ console.log(`\nTotal: ${data.total} | Failed: ${data.failedCount}`);
2170
+ result = data;
2171
+ }
2172
+ else {
2173
+ result = await getNetwork({
2174
+ filter: getArg('--filter'),
2175
+ });
2176
+ }
2013
2177
  break;
2178
+ }
2014
2179
  case 'refresh':
2015
2180
  result = await refresh({
2016
2181
  hard: hasFlag('--hard'),
@@ -2083,6 +2248,177 @@ async function handleStatusCommand() {
2083
2248
  execFileSync('node', [setupScript], { stdio: 'inherit' });
2084
2249
  break;
2085
2250
  }
2251
+ case 'console': {
2252
+ // Route to daemon ring buffer when daemon is alive
2253
+ const projRoot = findProjectRoot();
2254
+ const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2255
+ const errorsOnly = hasFlag('--errors');
2256
+ const lastN = getArg('--last') ? parseInt(getArg('--last'), 10) : undefined;
2257
+ const state = await ensureDaemon(projRoot, targetUrl);
2258
+ const resp = await daemonRequest(state, 'console-read', {
2259
+ errors: errorsOnly,
2260
+ last: lastN,
2261
+ });
2262
+ const data = resp.data;
2263
+ console.log(data.formatted);
2264
+ console.log(`\nTotal: ${data.total} | Errors: ${data.errorCount} | Warnings: ${data.warningCount}`);
2265
+ result = data;
2266
+ break;
2267
+ }
2268
+ case 'proof': {
2269
+ const prNum = getArg('--pr');
2270
+ if (!prNum) {
2271
+ console.error('[Sweetlink] Error: --pr <number> is required');
2272
+ process.exit(1);
2273
+ }
2274
+ const sessionDirArg = getArg('--session') ?? '.sweetlink';
2275
+ // Find latest session manifest
2276
+ const sessionFiles = fs.readdirSync(sessionDirArg)
2277
+ .filter((f) => f.startsWith('session-'))
2278
+ .sort()
2279
+ .reverse();
2280
+ if (sessionFiles.length === 0) {
2281
+ console.error('[Sweetlink] No session found. Run `record start` and `record stop` first.');
2282
+ process.exit(1);
2283
+ }
2284
+ const latestSession = path.join(sessionDirArg, sessionFiles[0]);
2285
+ const manifestPath = path.join(latestSession, 'sweetlink-session.json');
2286
+ if (!fs.existsSync(manifestPath)) {
2287
+ console.error(`[Sweetlink] No manifest found at ${manifestPath}`);
2288
+ process.exit(1);
2289
+ }
2290
+ const manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
2291
+ try {
2292
+ const { commentUrl } = await uploadEvidence(manifestData, latestSession, parseInt(prNum, 10), { repo: getArg('--repo') ?? undefined });
2293
+ console.log(`[Sweetlink] Evidence posted: ${commentUrl}`);
2294
+ result = { commentUrl };
2295
+ }
2296
+ catch (error) {
2297
+ console.error('[Sweetlink] Failed to upload evidence:', error instanceof Error ? error.message : error);
2298
+ process.exit(1);
2299
+ }
2300
+ break;
2301
+ }
2302
+ case 'record': {
2303
+ const projRoot = findProjectRoot();
2304
+ const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2305
+ const subcommand = args[1];
2306
+ const state = await ensureDaemon(projRoot, targetUrl);
2307
+ if (subcommand === 'start') {
2308
+ const resp = await daemonRequest(state, 'record-start');
2309
+ const data = resp.data;
2310
+ console.log(`[Sweetlink] Recording started: ${data.sessionId}`);
2311
+ result = data;
2312
+ }
2313
+ else if (subcommand === 'stop') {
2314
+ const resp = await daemonRequest(state, 'record-stop');
2315
+ const data = resp.data;
2316
+ console.log('[Sweetlink] Recording stopped.');
2317
+ console.log(JSON.stringify(data.manifest, null, 2));
2318
+ result = data;
2319
+ }
2320
+ else {
2321
+ const resp = await daemonRequest(state, 'record-status');
2322
+ const data = resp.data;
2323
+ if (data.recording) {
2324
+ console.log(`[Sweetlink] Recording in progress: ${data.sessionId} (${Math.round(data.duration ?? 0)}s, ${data.actionCount} actions)`);
2325
+ }
2326
+ else {
2327
+ console.log('[Sweetlink] No recording in progress.');
2328
+ }
2329
+ result = data;
2330
+ }
2331
+ break;
2332
+ }
2333
+ case 'daemon': {
2334
+ const subcommand = args[1];
2335
+ const projRoot = findProjectRoot();
2336
+ if (subcommand === 'stop') {
2337
+ const stopped = await stopDaemon(projRoot);
2338
+ console.log(stopped ? '[Sweetlink] Daemon stopped.' : '[Sweetlink] No daemon running.');
2339
+ result = { running: false };
2340
+ }
2341
+ else if (subcommand === 'start') {
2342
+ const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2343
+ const state = await ensureDaemon(projRoot, targetUrl);
2344
+ console.log(`[Sweetlink] Daemon running on port ${state.port} (PID: ${state.pid})`);
2345
+ result = {
2346
+ running: true,
2347
+ pid: state.pid,
2348
+ port: state.port,
2349
+ url: state.url,
2350
+ };
2351
+ }
2352
+ else {
2353
+ // Default: status
2354
+ const status = await getDaemonStatus(projRoot);
2355
+ if (status.running) {
2356
+ console.log(`[Sweetlink] Daemon running: port=${status.port} pid=${status.pid} uptime=${status.uptime}s`);
2357
+ }
2358
+ else {
2359
+ console.log('[Sweetlink] No daemon running.');
2360
+ }
2361
+ result = status;
2362
+ }
2363
+ break;
2364
+ }
2365
+ case 'fill': {
2366
+ const fillTarget = getArg('--selector') ?? args[1];
2367
+ const fillValue = getArg('--value') ?? args[2];
2368
+ if (!fillTarget) {
2369
+ console.error('[Sweetlink] Error: fill requires a target (@ref or --selector)');
2370
+ process.exit(1);
2371
+ }
2372
+ if (fillValue === undefined) {
2373
+ console.error('[Sweetlink] Error: fill requires a value (--value or positional arg)');
2374
+ process.exit(1);
2375
+ }
2376
+ if (/^@e\d+$/.test(fillTarget)) {
2377
+ const projRoot = findProjectRoot();
2378
+ const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2379
+ const state = await ensureDaemon(projRoot, targetUrl);
2380
+ await daemonRequest(state, 'fill-ref', { ref: fillTarget, value: fillValue });
2381
+ console.log(`[Sweetlink] Filled ${fillTarget} with "${fillValue}"`);
2382
+ result = { clicked: fillTarget, found: 1, index: 0 };
2383
+ }
2384
+ else {
2385
+ console.error('[Sweetlink] Error: fill currently only supports @e refs. Run `snapshot -i` first.');
2386
+ process.exit(1);
2387
+ }
2388
+ break;
2389
+ }
2390
+ case 'snapshot': {
2391
+ const projRoot = findProjectRoot();
2392
+ const targetUrl = getArg('--url') ?? 'http://localhost:3000';
2393
+ const interactive = hasFlag('-i') || hasFlag('--interactive');
2394
+ const doDiff = hasFlag('-D') || hasFlag('--diff');
2395
+ const doAnnotate = hasFlag('-a') || hasFlag('--annotate');
2396
+ const state = await ensureDaemon(projRoot, targetUrl);
2397
+ const resp = await daemonRequest(state, 'snapshot', {
2398
+ interactive,
2399
+ diff: doDiff,
2400
+ annotate: doAnnotate,
2401
+ });
2402
+ const data = resp.data;
2403
+ if (doDiff && data.diff) {
2404
+ console.log(data.diff);
2405
+ }
2406
+ else if (doAnnotate && data.screenshot) {
2407
+ const outputPath = getArg('--output') ?? getArg('-o') ?? 'annotated-snapshot.png';
2408
+ fs.writeFileSync(outputPath, Buffer.from(data.screenshot, 'base64'));
2409
+ console.log(`[Sweetlink] Annotated screenshot saved: ${outputPath}`);
2410
+ }
2411
+ else {
2412
+ console.log(data.tree);
2413
+ }
2414
+ console.log(`\n${data.count} elements found`);
2415
+ result = {
2416
+ tree: data.tree,
2417
+ refs: data.refs,
2418
+ diff: data.diff,
2419
+ };
2420
+ break;
2421
+ }
2086
2422
  default:
2087
2423
  console.error(`[Sweetlink] Unknown command: ${commandType}`);
2088
2424
  console.log('Run "pnpm sweetlink --help" for usage information');