@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.
- package/README.md +91 -12
- package/claude-skills/screenshot/SKILL.md +121 -20
- package/dist/cli/outputSchemas.d.ts +16 -0
- package/dist/cli/outputSchemas.d.ts.map +1 -1
- package/dist/cli/outputSchemas.js +33 -0
- package/dist/cli/outputSchemas.js.map +1 -1
- package/dist/cli/sweetlink-dev.js +0 -0
- package/dist/cli/sweetlink.js +347 -11
- package/dist/cli/sweetlink.js.map +1 -1
- package/dist/daemon/browser.d.ts +51 -0
- package/dist/daemon/browser.d.ts.map +1 -0
- package/dist/daemon/browser.js +153 -0
- package/dist/daemon/browser.js.map +1 -0
- package/dist/daemon/client.d.ts +32 -0
- package/dist/daemon/client.d.ts.map +1 -0
- package/dist/daemon/client.js +133 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/cursor.d.ts +15 -0
- package/dist/daemon/cursor.d.ts.map +1 -0
- package/dist/daemon/cursor.js +76 -0
- package/dist/daemon/cursor.js.map +1 -0
- package/dist/daemon/devices.d.ts +39 -0
- package/dist/daemon/devices.d.ts.map +1 -0
- package/dist/daemon/devices.js +101 -0
- package/dist/daemon/devices.js.map +1 -0
- package/dist/daemon/diff.d.ts +20 -0
- package/dist/daemon/diff.d.ts.map +1 -0
- package/dist/daemon/diff.js +181 -0
- package/dist/daemon/diff.js.map +1 -0
- package/dist/daemon/evidence.d.ts +29 -0
- package/dist/daemon/evidence.d.ts.map +1 -0
- package/dist/daemon/evidence.js +130 -0
- package/dist/daemon/evidence.js.map +1 -0
- package/dist/daemon/index.d.ts +10 -0
- package/dist/daemon/index.d.ts.map +1 -0
- package/dist/daemon/index.js +90 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/daemon/listeners.d.ts +55 -0
- package/dist/daemon/listeners.d.ts.map +1 -0
- package/dist/daemon/listeners.js +129 -0
- package/dist/daemon/listeners.js.map +1 -0
- package/dist/daemon/recording.d.ts +44 -0
- package/dist/daemon/recording.d.ts.map +1 -0
- package/dist/daemon/recording.js +133 -0
- package/dist/daemon/recording.js.map +1 -0
- package/dist/daemon/refs.d.ts +70 -0
- package/dist/daemon/refs.d.ts.map +1 -0
- package/dist/daemon/refs.js +185 -0
- package/dist/daemon/refs.js.map +1 -0
- package/dist/daemon/ringBuffer.d.ts +26 -0
- package/dist/daemon/ringBuffer.d.ts.map +1 -0
- package/dist/daemon/ringBuffer.js +54 -0
- package/dist/daemon/ringBuffer.js.map +1 -0
- package/dist/daemon/server.d.ts +23 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +508 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/session.d.ts +41 -0
- package/dist/daemon/session.d.ts.map +1 -0
- package/dist/daemon/session.js +8 -0
- package/dist/daemon/session.js.map +1 -0
- package/dist/daemon/stateFile.d.ts +49 -0
- package/dist/daemon/stateFile.d.ts.map +1 -0
- package/dist/daemon/stateFile.js +162 -0
- package/dist/daemon/stateFile.js.map +1 -0
- package/dist/daemon/types.d.ts +72 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/types.js +28 -0
- package/dist/daemon/types.js.map +1 -0
- package/dist/daemon/viewer.d.ts +33 -0
- package/dist/daemon/viewer.d.ts.map +1 -0
- package/dist/daemon/viewer.js +226 -0
- package/dist/daemon/viewer.js.map +1 -0
- package/dist/daemon/visualDiff.d.ts +34 -0
- package/dist/daemon/visualDiff.d.ts.map +1 -0
- package/dist/daemon/visualDiff.js +80 -0
- package/dist/daemon/visualDiff.js.map +1 -0
- package/package.json +20 -12
package/dist/cli/sweetlink.js
CHANGED
|
@@ -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
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
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
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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');
|