@ytspar/sweetlink 1.26.2 → 1.26.3-canary.66f78a4
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 +21 -1
- package/dist/browser/SweetlinkBridge.d.ts +0 -1
- package/dist/browser/SweetlinkBridge.d.ts.map +1 -1
- package/dist/browser/SweetlinkBridge.js +33 -29
- package/dist/browser/SweetlinkBridge.js.map +1 -1
- package/dist/browser/commands/screenshot.d.ts.map +1 -1
- package/dist/browser/commands/screenshot.js +3 -0
- package/dist/browser/commands/screenshot.js.map +1 -1
- package/dist/cli/clickCode.d.ts +35 -0
- package/dist/cli/clickCode.d.ts.map +1 -0
- package/dist/cli/clickCode.js +68 -0
- package/dist/cli/clickCode.js.map +1 -0
- package/dist/cli/sweetlink.js +168 -57
- package/dist/cli/sweetlink.js.map +1 -1
- package/dist/daemon/browser.d.ts +7 -1
- package/dist/daemon/browser.d.ts.map +1 -1
- package/dist/daemon/browser.js +11 -7
- package/dist/daemon/browser.js.map +1 -1
- package/dist/daemon/index.js +21 -6
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/server.d.ts +2 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +26 -5
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/types.d.ts +4 -0
- package/dist/daemon/types.d.ts.map +1 -1
- package/dist/daemon/types.js.map +1 -1
- package/dist/next.d.ts +8 -0
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +12 -2
- package/dist/next.js.map +1 -1
- package/dist/server/discovery.d.ts +52 -0
- package/dist/server/discovery.d.ts.map +1 -0
- package/dist/server/discovery.js +102 -0
- package/dist/server/discovery.js.map +1 -0
- package/dist/server/index.d.ts +22 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +145 -31
- package/dist/server/index.js.map +1 -1
- package/dist/types.d.ts +66 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +110 -9
- package/dist/types.js.map +1 -1
- package/dist/urlUtils.d.ts +35 -0
- package/dist/urlUtils.d.ts.map +1 -1
- package/dist/urlUtils.js +77 -0
- package/dist/urlUtils.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +5 -4
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/sweetlink.js
CHANGED
|
@@ -16,8 +16,10 @@ import { extractPort } from '../daemon/stateFile.js';
|
|
|
16
16
|
import { ensureDir } from '../daemon/utils.js';
|
|
17
17
|
import { screenshotViaPlaywright } from '../playwright.js';
|
|
18
18
|
import { getCardHeaderPreset, getNavigationPreset, measureViaPlaywright } from '../ruler.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { findServerInfoFile } from '../server/discovery.js';
|
|
20
|
+
import { DEFAULT_WS_PORT, MAX_PORT_RETRIES, parseLocalDevelopmentUrl, parsePortNumber, resolveSweetlinkWsPortForAppPort, } from '../types.js';
|
|
21
|
+
import { SCREENSHOT_DIR, selectClientForTargetUrl, urlsEquivalent } from '../urlUtils.js';
|
|
22
|
+
import { generateClickCode } from './clickCode.js';
|
|
21
23
|
import { emitJson, printOutputSchema } from './outputSchemas.js';
|
|
22
24
|
const COMMON_APP_PORTS = [3000, 3001, 4000, 5173, 5174, 8000, 8080];
|
|
23
25
|
/**
|
|
@@ -209,7 +211,7 @@ function reportScreenshotSuccess(outputPath, width, height, method, selector) {
|
|
|
209
211
|
}
|
|
210
212
|
}
|
|
211
213
|
let resolvedWsUrl = null;
|
|
212
|
-
const DEFAULT_WS_URL =
|
|
214
|
+
const DEFAULT_WS_URL = 'ws://localhost:9223';
|
|
213
215
|
const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
|
214
216
|
const SERVER_READY_TIMEOUT = 30000; // 30 seconds to wait for server
|
|
215
217
|
const SERVER_POLL_INTERVAL = 500; // Poll every 500ms
|
|
@@ -234,6 +236,7 @@ async function probeServerIdentity(port) {
|
|
|
234
236
|
return {
|
|
235
237
|
port,
|
|
236
238
|
appPort: data.appPort ?? null,
|
|
239
|
+
pid: typeof data.pid === 'number' ? data.pid : null,
|
|
237
240
|
gitBranch: data.gitBranch ?? null,
|
|
238
241
|
appName: data.appName ?? null,
|
|
239
242
|
};
|
|
@@ -252,9 +255,9 @@ async function discoverServer(target) {
|
|
|
252
255
|
for (let port = SCAN_PORT_START; port <= SCAN_PORT_END; port++) {
|
|
253
256
|
probes.push(probeServerIdentity(port));
|
|
254
257
|
}
|
|
255
|
-
// Also probe common offset ports (appPort + 6223)
|
|
258
|
+
// Also probe common offset ports (appPort + 6223, skipping unsafe ports)
|
|
256
259
|
for (const appPort of COMMON_APP_PORTS) {
|
|
257
|
-
const wsPort = appPort
|
|
260
|
+
const wsPort = resolveSweetlinkWsPortForAppPort(appPort);
|
|
258
261
|
if (wsPort < SCAN_PORT_START || wsPort > SCAN_PORT_END) {
|
|
259
262
|
probes.push(probeServerIdentity(wsPort));
|
|
260
263
|
}
|
|
@@ -280,12 +283,58 @@ async function discoverServer(target) {
|
|
|
280
283
|
throw new Error(`No server matching "${target}" found.\nAvailable servers:\n${available}`);
|
|
281
284
|
}
|
|
282
285
|
/**
|
|
283
|
-
* Resolve
|
|
284
|
-
*
|
|
286
|
+
* Resolve this project's server from `.sweetlink/server.json` (written by
|
|
287
|
+
* the server at startup; walked up from cwd). The file can be stale after a
|
|
288
|
+
* crash, so it is only trusted when the info endpoint on that port answers
|
|
289
|
+
* as a sweetlink server whose pid/appPort agree with the file.
|
|
290
|
+
*/
|
|
291
|
+
async function resolveWsUrlFromServerInfoFile() {
|
|
292
|
+
const found = findServerInfoFile(process.cwd());
|
|
293
|
+
if (!found)
|
|
294
|
+
return null;
|
|
295
|
+
const { info } = found;
|
|
296
|
+
// With an explicit --url port that disagrees with the file's app port,
|
|
297
|
+
// the user means a different server — fall through to port math.
|
|
298
|
+
const urlArg = getArg('--url');
|
|
299
|
+
const urlPort = urlArg ? parsePortNumber(parseLocalDevelopmentUrl(urlArg)?.port) : null;
|
|
300
|
+
if (urlPort && info.appPort !== null && info.appPort !== urlPort)
|
|
301
|
+
return null;
|
|
302
|
+
const identity = await probeServerIdentity(info.wsPort);
|
|
303
|
+
if (!identity)
|
|
304
|
+
return null; // stale — nothing listening there anymore
|
|
305
|
+
if (identity.pid !== null && identity.pid !== info.pid)
|
|
306
|
+
return null; // port reused
|
|
307
|
+
if (info.appPort !== null && identity.appPort !== null && identity.appPort !== info.appPort) {
|
|
308
|
+
return null; // port reused by another project's server
|
|
309
|
+
}
|
|
310
|
+
console.log(`[Sweetlink] Using server from ${path.relative(process.cwd(), found.filePath) || found.filePath} (ws://localhost:${info.wsPort})`);
|
|
311
|
+
return `ws://localhost:${info.wsPort}`;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Resolve the WebSocket URL to use, in order:
|
|
315
|
+
* 1. SWEETLINK_WS_URL env (explicit override)
|
|
316
|
+
* 2. A live `.sweetlink/server.json` written by this project's server
|
|
317
|
+
* 3. Port math from an explicit --url port (app port + offset)
|
|
318
|
+
* 4. The default ws://localhost:9223
|
|
285
319
|
*/
|
|
286
320
|
async function getWsUrl() {
|
|
287
321
|
if (resolvedWsUrl)
|
|
288
322
|
return resolvedWsUrl;
|
|
323
|
+
if (process.env.SWEETLINK_WS_URL) {
|
|
324
|
+
resolvedWsUrl = process.env.SWEETLINK_WS_URL;
|
|
325
|
+
return resolvedWsUrl;
|
|
326
|
+
}
|
|
327
|
+
const discovered = await resolveWsUrlFromServerInfoFile();
|
|
328
|
+
if (discovered) {
|
|
329
|
+
resolvedWsUrl = discovered;
|
|
330
|
+
return resolvedWsUrl;
|
|
331
|
+
}
|
|
332
|
+
const urlArg = getArg('--url');
|
|
333
|
+
const urlPort = urlArg ? parsePortNumber(parseLocalDevelopmentUrl(urlArg)?.port) : null;
|
|
334
|
+
if (urlPort) {
|
|
335
|
+
resolvedWsUrl = `ws://localhost:${resolveSweetlinkWsPortForAppPort(urlPort)}`;
|
|
336
|
+
return resolvedWsUrl;
|
|
337
|
+
}
|
|
289
338
|
resolvedWsUrl = DEFAULT_WS_URL;
|
|
290
339
|
return resolvedWsUrl;
|
|
291
340
|
}
|
|
@@ -347,8 +396,67 @@ async function waitForServer(url, timeout = SERVER_READY_TIMEOUT) {
|
|
|
347
396
|
}
|
|
348
397
|
throw new Error(`Server not ready after ${timeout}ms: ${lastError?.message || 'Connection refused'}`);
|
|
349
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* One-shot preflight for --url commands: verify a browser client that could
|
|
401
|
+
* plausibly be on the target page is attached to the server before
|
|
402
|
+
* dispatching, and surface an actionable error (listing the connected
|
|
403
|
+
* clients' locations) when every client is on a different page/origin.
|
|
404
|
+
* Old servers without per-client info skip the check silently.
|
|
405
|
+
*/
|
|
406
|
+
let targetClientPreflightDone = false;
|
|
407
|
+
async function preflightTargetClients(targetUrl) {
|
|
408
|
+
if (targetClientPreflightDone)
|
|
409
|
+
return;
|
|
410
|
+
targetClientPreflightDone = true;
|
|
411
|
+
let failure = null;
|
|
412
|
+
try {
|
|
413
|
+
const wsUrl = await getWsUrl();
|
|
414
|
+
const httpUrl = wsUrl.replace(/^ws:\/\//, 'http://').replace(/^wss:\/\//, 'https://');
|
|
415
|
+
const controller = new AbortController();
|
|
416
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
417
|
+
const response = await fetch(httpUrl, { signal: controller.signal });
|
|
418
|
+
clearTimeout(timeoutId);
|
|
419
|
+
if (!response.ok)
|
|
420
|
+
return;
|
|
421
|
+
const data = (await response.json());
|
|
422
|
+
if (!Array.isArray(data?.clients))
|
|
423
|
+
return; // old server — no visibility
|
|
424
|
+
const browserClients = data.clients.filter((c) => c?.type === 'browser');
|
|
425
|
+
if (browserClients.length === 0)
|
|
426
|
+
return; // "No browser client" paths handle this
|
|
427
|
+
const { match } = selectClientForTargetUrl(browserClients, targetUrl);
|
|
428
|
+
if (match === 'none') {
|
|
429
|
+
const locations = browserClients
|
|
430
|
+
.map((c) => c.url ?? c.origin ?? 'unknown location')
|
|
431
|
+
.join(', ');
|
|
432
|
+
failure =
|
|
433
|
+
`No connected browser client matches ${targetUrl}. ` +
|
|
434
|
+
`Connected client(s): ${locations}. ` +
|
|
435
|
+
'Open the page in a browser (or fix --url) and retry.';
|
|
436
|
+
}
|
|
437
|
+
else if (match === 'unknown-location') {
|
|
438
|
+
console.warn(`[Sweetlink] ⚠ Connected browser client has not reported its location ` +
|
|
439
|
+
`(older devbar build?) — cannot verify it is on ${targetUrl}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// Info endpoint unreachable or unparseable — fall through to normal
|
|
444
|
+
// command dispatch, which has its own failure handling.
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (failure) {
|
|
448
|
+
console.error(`[Sweetlink] ${failure}`);
|
|
449
|
+
throw new Error(failure);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
350
452
|
async function sendCommand(command, timeoutMs = DEFAULT_TIMEOUT) {
|
|
351
453
|
const wsUrl = await getWsUrl();
|
|
454
|
+
// Target the client at --url so a server with several attached pages
|
|
455
|
+
// (multi-project setups) never dispatches this command to a foreign page.
|
|
456
|
+
const targetUrl = getArg('--url');
|
|
457
|
+
if (targetUrl)
|
|
458
|
+
await preflightTargetClients(targetUrl);
|
|
459
|
+
const payload = targetUrl ? { ...command, targetUrl } : command;
|
|
352
460
|
return new Promise((resolve, reject) => {
|
|
353
461
|
const ws = new WebSocket(wsUrl);
|
|
354
462
|
const timer = setTimeout(() => {
|
|
@@ -356,7 +464,7 @@ async function sendCommand(command, timeoutMs = DEFAULT_TIMEOUT) {
|
|
|
356
464
|
reject(new Error('Command timeout - is the dev server running?'));
|
|
357
465
|
}, timeoutMs);
|
|
358
466
|
ws.on('open', () => {
|
|
359
|
-
ws.send(JSON.stringify(
|
|
467
|
+
ws.send(JSON.stringify(payload));
|
|
360
468
|
});
|
|
361
469
|
ws.on('message', (data) => {
|
|
362
470
|
clearTimeout(timer);
|
|
@@ -371,13 +479,6 @@ async function sendCommand(command, timeoutMs = DEFAULT_TIMEOUT) {
|
|
|
371
479
|
});
|
|
372
480
|
});
|
|
373
481
|
}
|
|
374
|
-
/**
|
|
375
|
-
* Compare two URLs ignoring trailing slashes.
|
|
376
|
-
* Exported-style name for testability (re-implemented in tests).
|
|
377
|
-
*/
|
|
378
|
-
function urlsMatch(a, b) {
|
|
379
|
-
return a.replace(/\/+$/, '') === b.replace(/\/+$/, '');
|
|
380
|
-
}
|
|
381
482
|
const NAVIGATE_POLL_INTERVAL = 500; // ms between reconnection polls
|
|
382
483
|
const NAVIGATE_DEFAULT_TIMEOUT = 10000; // 10s default
|
|
383
484
|
/**
|
|
@@ -394,7 +495,7 @@ async function navigateBrowser(url, timeout = NAVIGATE_DEFAULT_TIMEOUT) {
|
|
|
394
495
|
const response = await sendCommand({ type: 'exec-js', code: 'window.location.href' }, 3000);
|
|
395
496
|
if (response.success && response.data != null) {
|
|
396
497
|
const currentUrl = String(response.data.result ?? response.data);
|
|
397
|
-
if (
|
|
498
|
+
if (urlsEquivalent(currentUrl, url)) {
|
|
398
499
|
console.log(`[Sweetlink] Browser already on ${url}`);
|
|
399
500
|
return true;
|
|
400
501
|
}
|
|
@@ -423,7 +524,7 @@ async function navigateBrowser(url, timeout = NAVIGATE_DEFAULT_TIMEOUT) {
|
|
|
423
524
|
const response = await sendCommand({ type: 'exec-js', code: 'window.location.href' }, 3000);
|
|
424
525
|
if (response.success && response.data != null) {
|
|
425
526
|
const currentUrl = String(response.data.result ?? response.data);
|
|
426
|
-
if (
|
|
527
|
+
if (urlsEquivalent(currentUrl, url)) {
|
|
427
528
|
// Give devbar time to fully initialize after page load
|
|
428
529
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
429
530
|
console.log(`[Sweetlink] Browser reconnected on ${url}`);
|
|
@@ -523,6 +624,9 @@ async function screenshot(options) {
|
|
|
523
624
|
const resp = await daemonRequest(daemonState, 'screenshot-responsive', {
|
|
524
625
|
fullPage: options.fullPage,
|
|
525
626
|
hideDevbar: options.hideDevbar,
|
|
627
|
+
// Explicit --url only: the daemon re-navigates when the live page
|
|
628
|
+
// differs (SPA routing may have moved it since the last command).
|
|
629
|
+
url: options.url,
|
|
526
630
|
});
|
|
527
631
|
const data = resp.data;
|
|
528
632
|
const outputDir = options.output
|
|
@@ -554,6 +658,9 @@ async function screenshot(options) {
|
|
|
554
658
|
padding: options.padding,
|
|
555
659
|
theme: options.theme,
|
|
556
660
|
hideDevbar: options.hideDevbar,
|
|
661
|
+
// Explicit --url only: the daemon re-navigates when the live page
|
|
662
|
+
// differs (SPA routing may have moved it since the last command).
|
|
663
|
+
url: options.url,
|
|
557
664
|
});
|
|
558
665
|
const data = resp.data;
|
|
559
666
|
const outputPath = options.output || getDefaultScreenshotPath();
|
|
@@ -668,10 +775,25 @@ async function screenshot(options) {
|
|
|
668
775
|
console.error('[Sweetlink] Screenshot failed:', response.error);
|
|
669
776
|
process.exit(1);
|
|
670
777
|
}
|
|
778
|
+
const data = response.data;
|
|
779
|
+
// Tier-1 safety: the browser client reports the URL it actually
|
|
780
|
+
// captured. If --url was requested and the connected client is on a
|
|
781
|
+
// different page (e.g. a zombie headless page left behind by a stale
|
|
782
|
+
// daemon), never save the wrong capture silently — escalate to
|
|
783
|
+
// Playwright, which navigates for real.
|
|
784
|
+
const capturedUrl = typeof data.url === 'string' ? data.url : null;
|
|
785
|
+
if (options.url && capturedUrl && !urlsEquivalent(capturedUrl, options.url)) {
|
|
786
|
+
console.warn(`[Sweetlink] ⚠ Connected browser is on ${capturedUrl}, not ${options.url} — discarding WebSocket capture`);
|
|
787
|
+
if (options.forceWS) {
|
|
788
|
+
console.error('[Sweetlink] Refusing to save a capture of the wrong page (--force-ws prevents Playwright fallback)');
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
console.log('[Sweetlink] Escalating to Playwright for the requested URL');
|
|
792
|
+
return await takePlaywrightScreenshot(playwrightOpts, 'Playwright (URL mismatch fallback)');
|
|
793
|
+
}
|
|
671
794
|
// Save screenshot
|
|
672
795
|
const outputPath = options.output || getDefaultScreenshotPath();
|
|
673
796
|
ensureDir(outputPath);
|
|
674
|
-
const data = response.data;
|
|
675
797
|
const base64Data = data.screenshot.replace(/^data:image\/png;base64,/, '');
|
|
676
798
|
fs.writeFileSync(outputPath, Buffer.from(base64Data, 'base64'));
|
|
677
799
|
reportScreenshotSuccess(outputPath, data.width, data.height, 'WebSocket (html2canvas)', data.selector);
|
|
@@ -1017,39 +1139,6 @@ async function execJS(options) {
|
|
|
1017
1139
|
process.exit(1);
|
|
1018
1140
|
}
|
|
1019
1141
|
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Generate JavaScript code that finds elements and clicks the one at the given index.
|
|
1022
|
-
* Parameterized by the element-finding strategy: text content search or CSS selector.
|
|
1023
|
-
*/
|
|
1024
|
-
function generateClickCode(strategy, index) {
|
|
1025
|
-
// The element-finding expression differs, but the bounds-check + click + return is shared
|
|
1026
|
-
const escapedSelector = JSON.stringify(strategy.selector);
|
|
1027
|
-
let findExpression;
|
|
1028
|
-
let notFoundMsg;
|
|
1029
|
-
if (strategy.type === 'text') {
|
|
1030
|
-
const escapedText = JSON.stringify(strategy.text);
|
|
1031
|
-
findExpression = `Array.from(document.querySelectorAll(${escapedSelector})).filter(el => el.textContent?.includes(${escapedText}))`;
|
|
1032
|
-
notFoundMsg = `"No element found with text: " + ${escapedText}`;
|
|
1033
|
-
}
|
|
1034
|
-
else {
|
|
1035
|
-
findExpression = `Array.from(document.querySelectorAll(${escapedSelector}))`;
|
|
1036
|
-
notFoundMsg = `"No element found matching: " + ${escapedSelector}`;
|
|
1037
|
-
}
|
|
1038
|
-
return `
|
|
1039
|
-
(() => {
|
|
1040
|
-
const elements = ${findExpression};
|
|
1041
|
-
if (elements.length === 0) {
|
|
1042
|
-
return { success: false, error: ${notFoundMsg} };
|
|
1043
|
-
}
|
|
1044
|
-
const target = elements[${index}];
|
|
1045
|
-
if (!target) {
|
|
1046
|
-
return { success: false, error: "Index ${index} out of bounds, found " + elements.length + " elements" };
|
|
1047
|
-
}
|
|
1048
|
-
target.click();
|
|
1049
|
-
return { success: true, clicked: target.tagName + (target.className ? "." + target.className.split(" ")[0] : ""), found: elements.length };
|
|
1050
|
-
})()
|
|
1051
|
-
`;
|
|
1052
|
-
}
|
|
1053
1142
|
async function click(options) {
|
|
1054
1143
|
const { selector, text, index = 0 } = options;
|
|
1055
1144
|
if (!selector && !text) {
|
|
@@ -1059,9 +1148,14 @@ async function click(options) {
|
|
|
1059
1148
|
let clickCode;
|
|
1060
1149
|
let description;
|
|
1061
1150
|
if (text) {
|
|
1062
|
-
const baseSelector = selector || '*';
|
|
1063
1151
|
description = selector ? `"${text}" within ${selector}` : `"${text}"`;
|
|
1064
|
-
|
|
1152
|
+
// Explicit selector: the user chose the candidate scoping. Bare --text:
|
|
1153
|
+
// leaf-most + clickable-preferred matching (see clickCode.ts) so the
|
|
1154
|
+
// click lands on the button around the text, not on <html> (whose
|
|
1155
|
+
// textContent also matches).
|
|
1156
|
+
clickCode = selector
|
|
1157
|
+
? generateClickCode({ type: 'text', text, selector }, index)
|
|
1158
|
+
: generateClickCode({ type: 'smart-text', text }, index);
|
|
1065
1159
|
}
|
|
1066
1160
|
else {
|
|
1067
1161
|
description = `${selector}${index > 0 ? ` [${index}]` : ''}`;
|
|
@@ -1288,7 +1382,7 @@ function getPortsToScan() {
|
|
|
1288
1382
|
}
|
|
1289
1383
|
// Common app ports + offset (e.g., 3000 -> 9223, 5173 -> 11396)
|
|
1290
1384
|
for (const appPort of COMMON_APP_PORTS) {
|
|
1291
|
-
const wsPort = appPort
|
|
1385
|
+
const wsPort = resolveSweetlinkWsPortForAppPort(appPort);
|
|
1292
1386
|
for (let i = 0; i <= MAX_PORT_RETRIES; i++) {
|
|
1293
1387
|
ports.add(wsPort + i);
|
|
1294
1388
|
}
|
|
@@ -2383,6 +2477,17 @@ function getArg(flag) {
|
|
|
2383
2477
|
function hasFlag(flag) {
|
|
2384
2478
|
return args.includes(flag);
|
|
2385
2479
|
}
|
|
2480
|
+
/**
|
|
2481
|
+
* Positional argument at `index`, or undefined when that token is a flag.
|
|
2482
|
+
* Flags must never be consumed as positionals: `click --text "Save list"`
|
|
2483
|
+
* used to take the literal `--text` as the positional selector, producing
|
|
2484
|
+
* querySelectorAll("--text") and "No element found" for every documented
|
|
2485
|
+
* `click --text ...` invocation.
|
|
2486
|
+
*/
|
|
2487
|
+
function getPositionalArg(index) {
|
|
2488
|
+
const value = args[index];
|
|
2489
|
+
return value !== undefined && !value.startsWith('--') ? value : undefined;
|
|
2490
|
+
}
|
|
2386
2491
|
// Per-command --help: `pnpm sweetlink screenshot --help`
|
|
2387
2492
|
// Past the early-exit at the top of dispatch, commandType is non-null.
|
|
2388
2493
|
if (hasFlag('--help') || hasFlag('-h')) {
|
|
@@ -2572,6 +2677,9 @@ async function handleInspectCmd() {
|
|
|
2572
2677
|
expectedOutcome: getArg('--expected'),
|
|
2573
2678
|
actionTranscript,
|
|
2574
2679
|
includeA11y: !hasFlag('--no-a11y'),
|
|
2680
|
+
// Explicit --url only: the daemon re-navigates when the live page
|
|
2681
|
+
// differs (SPA routing may have moved it since the last command).
|
|
2682
|
+
url: getArg('--url'),
|
|
2575
2683
|
});
|
|
2576
2684
|
const data = resp.data;
|
|
2577
2685
|
const output = getArg('--output');
|
|
@@ -2636,7 +2744,7 @@ async function handleExecCmd() {
|
|
|
2636
2744
|
});
|
|
2637
2745
|
}
|
|
2638
2746
|
async function handleClickCmd() {
|
|
2639
|
-
const clickTarget = getArg('--selector') ??
|
|
2747
|
+
const clickTarget = getArg('--selector') ?? getPositionalArg(1);
|
|
2640
2748
|
const clickText = getArg('--text');
|
|
2641
2749
|
const clickIndex = getArg('--index') ? parseInt(getArg('--index'), 10) : 0;
|
|
2642
2750
|
const projRoot = findProjectRoot();
|
|
@@ -3007,8 +3115,8 @@ async function handleDaemonCmd() {
|
|
|
3007
3115
|
return status;
|
|
3008
3116
|
}
|
|
3009
3117
|
async function handleFillCmd() {
|
|
3010
|
-
const fillTarget = getArg('--selector') ??
|
|
3011
|
-
const fillValue = getArg('--value') ??
|
|
3118
|
+
const fillTarget = getArg('--selector') ?? getPositionalArg(1);
|
|
3119
|
+
const fillValue = getArg('--value') ?? getPositionalArg(2);
|
|
3012
3120
|
if (!fillTarget) {
|
|
3013
3121
|
console.error('[Sweetlink] Error: fill requires a target (@ref or --selector)');
|
|
3014
3122
|
process.exit(1);
|
|
@@ -3039,6 +3147,9 @@ async function handleSnapshotCmd() {
|
|
|
3039
3147
|
interactive,
|
|
3040
3148
|
diff: doDiff,
|
|
3041
3149
|
annotate: doAnnotate,
|
|
3150
|
+
// Explicit --url only: the daemon re-navigates when the live page
|
|
3151
|
+
// differs (SPA routing may have moved it since the last command).
|
|
3152
|
+
url: getArg('--url'),
|
|
3042
3153
|
});
|
|
3043
3154
|
const data = resp.data;
|
|
3044
3155
|
if (doDiff && data.diff) {
|