@ytspar/sweetlink 1.26.6 → 1.27.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/dist/batchManifest.d.ts +35 -0
- package/dist/batchManifest.d.ts.map +1 -0
- package/dist/batchManifest.js +45 -0
- package/dist/batchManifest.js.map +1 -0
- package/dist/cdp.d.ts.map +1 -1
- package/dist/cdp.js +2 -1
- package/dist/cdp.js.map +1 -1
- package/dist/cli/sweetlink.js +204 -71
- package/dist/cli/sweetlink.js.map +1 -1
- package/dist/daemon/browser.d.ts.map +1 -1
- package/dist/daemon/browser.js +0 -2
- package/dist/daemon/browser.js.map +1 -1
- package/dist/daemon/index.js +2 -1
- package/dist/daemon/index.js.map +1 -1
- package/dist/playwright.d.ts.map +1 -1
- package/dist/playwright.js +17 -40
- package/dist/playwright.js.map +1 -1
- package/dist/urlUtils.d.ts +19 -6
- package/dist/urlUtils.d.ts.map +1 -1
- package/dist/urlUtils.js +21 -9
- package/dist/urlUtils.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch screenshot manifest parsing.
|
|
3
|
+
*
|
|
4
|
+
* Lives outside the CLI entry point so the validation is directly importable and
|
|
5
|
+
* testable — the CLI module is a top-level IIFE and cannot be imported for a
|
|
6
|
+
* unit test without running it.
|
|
7
|
+
*/
|
|
8
|
+
/** One frame in a `--batch` manifest. `url` and `output` are required. */
|
|
9
|
+
export interface BatchScreenshotItem {
|
|
10
|
+
url: string;
|
|
11
|
+
output: string;
|
|
12
|
+
selector?: string;
|
|
13
|
+
fullPage?: boolean;
|
|
14
|
+
viewport?: string;
|
|
15
|
+
hideDevbar?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface BatchScreenshotResult {
|
|
18
|
+
url: string;
|
|
19
|
+
output: string;
|
|
20
|
+
ok: boolean;
|
|
21
|
+
width?: number;
|
|
22
|
+
height?: number;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse and validate a batch manifest.
|
|
27
|
+
*
|
|
28
|
+
* Every field is checked up front rather than per frame, so a malformed manifest
|
|
29
|
+
* fails before the first browser is touched instead of halfway through a sweep.
|
|
30
|
+
* `output` in particular is required: defaulting it would make every frame write
|
|
31
|
+
* to the same path, and a directory of identical frames reads downstream as "the
|
|
32
|
+
* capture tool duplicated my frames" rather than "my manifest was malformed".
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseBatchManifest(raw: string): BatchScreenshotItem[];
|
|
35
|
+
//# sourceMappingURL=batchManifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batchManifest.d.ts","sourceRoot":"","sources":["../src/batchManifest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,mBAAmB;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,EAAE,CA4BrE"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch screenshot manifest parsing.
|
|
3
|
+
*
|
|
4
|
+
* Lives outside the CLI entry point so the validation is directly importable and
|
|
5
|
+
* testable — the CLI module is a top-level IIFE and cannot be imported for a
|
|
6
|
+
* unit test without running it.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Parse and validate a batch manifest.
|
|
10
|
+
*
|
|
11
|
+
* Every field is checked up front rather than per frame, so a malformed manifest
|
|
12
|
+
* fails before the first browser is touched instead of halfway through a sweep.
|
|
13
|
+
* `output` in particular is required: defaulting it would make every frame write
|
|
14
|
+
* to the same path, and a directory of identical frames reads downstream as "the
|
|
15
|
+
* capture tool duplicated my frames" rather than "my manifest was malformed".
|
|
16
|
+
*/
|
|
17
|
+
export function parseBatchManifest(raw) {
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new Error(`batch manifest is not valid JSON: ${error instanceof Error ? error.message : error}`);
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(parsed)) {
|
|
26
|
+
throw new Error('batch manifest must be a JSON array of { url, output } items');
|
|
27
|
+
}
|
|
28
|
+
if (parsed.length === 0) {
|
|
29
|
+
throw new Error('batch manifest is empty — nothing to capture');
|
|
30
|
+
}
|
|
31
|
+
return parsed.map((item, i) => {
|
|
32
|
+
if (typeof item !== 'object' || item === null) {
|
|
33
|
+
throw new Error(`batch item ${i} is not an object`);
|
|
34
|
+
}
|
|
35
|
+
const { url, output } = item;
|
|
36
|
+
if (typeof url !== 'string' || url.length === 0) {
|
|
37
|
+
throw new Error(`batch item ${i} is missing a "url"`);
|
|
38
|
+
}
|
|
39
|
+
if (typeof output !== 'string' || output.length === 0) {
|
|
40
|
+
throw new Error(`batch item ${i} (${url}) is missing an "output" path`);
|
|
41
|
+
}
|
|
42
|
+
return item;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=batchManifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batchManifest.js","sourceRoot":"","sources":["../src/batchManifest.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqBH;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,qCAAqC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CACtF,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,8DAA8D,CAAC,CAAC;IAClF,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;QAC5B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC;QACtD,CAAC;QACD,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAA+B,CAAC;QACxD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChD,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,qBAAqB,CAAC,CAAC;QACxD,CAAC;QACD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtD,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,KAAK,GAAG,+BAA+B,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,IAA2B,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/cdp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cdp.d.ts","sourceRoot":"","sources":["../src/cdp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,OAAO,EAA6B,IAAI,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"cdp.d.ts","sourceRoot":"","sources":["../src/cdp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,OAAO,EAA6B,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAsB/E;;GAEG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAOlD;AAED;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CActD;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BtE;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,oBAAoB,EAAE,YAAY,GAAG,MAAM,CAAC;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CA2H7D;AAKD;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAgCjE;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CACpF,KAAK,CAAC;IACJ,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CACH,CAmDA"}
|
package/dist/cdp.js
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import { HIDE_DEVBAR_CSS, HIDE_DEVBAR_STYLE_ID, HOVER_TRANSITION_DELAY_MS, SELECTOR_TIMEOUT_MS, } from './screenshotConstants.js';
|
|
9
|
+
import { defaultDevUrl } from './urlUtils.js';
|
|
9
10
|
import { parseViewport } from './viewportUtils.js';
|
|
10
11
|
// ============================================================================
|
|
11
12
|
// Constants
|
|
12
13
|
// ============================================================================
|
|
13
14
|
const CDP_URL = process.env.CHROME_CDP_URL || 'http://127.0.0.1:9222';
|
|
14
|
-
const DEFAULT_DEV_URL =
|
|
15
|
+
const DEFAULT_DEV_URL = defaultDevUrl();
|
|
15
16
|
/** Timeouts */
|
|
16
17
|
const NETWORK_IDLE_TIMEOUT_MS = 10000;
|
|
17
18
|
const NETWORK_IDLE_TIME_MS = 500;
|
package/dist/cdp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cdp.js","sourceRoot":"","sources":["../src/cdp.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AAEzB,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,+EAA+E;AAC/E,YAAY;AACZ,+EAA+E;AAE/E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,uBAAuB,CAAC;AACtE,MAAM,eAAe,GAAG,
|
|
1
|
+
{"version":3,"file":"cdp.js","sourceRoot":"","sources":["../src/cdp.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/sweetlink.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as childProcess from 'child_process';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
|
-
import * as https from 'https';
|
|
11
10
|
import * as path from 'path';
|
|
12
11
|
import { WebSocket } from 'ws';
|
|
12
|
+
import { parseBatchManifest, } from '../batchManifest.js';
|
|
13
13
|
import { detectCDP, getNetworkRequestsViaCDP } from '../cdp.js';
|
|
14
14
|
import { DaemonRequestError, daemonRequest, ensureDaemon, getDaemonStatus, stopDaemon, } from '../daemon/client.js';
|
|
15
15
|
import { uploadEvidence } from '../daemon/evidence.js';
|
|
@@ -19,7 +19,7 @@ import { screenshotViaPlaywright } from '../playwright.js';
|
|
|
19
19
|
import { getCardHeaderPreset, getNavigationPreset, measureViaPlaywright } from '../ruler.js';
|
|
20
20
|
import { findServerInfoFile } from '../server/discovery.js';
|
|
21
21
|
import { DEFAULT_WS_PORT, MAX_PORT_RETRIES, parseLocalDevelopmentUrl, parsePortNumber, resolveSweetlinkWsPortForAppPort, } from '../types.js';
|
|
22
|
-
import {
|
|
22
|
+
import { defaultDevUrl, SCREENSHOT_DIR, selectClientForTargetUrl, urlsEquivalent, } from '../urlUtils.js';
|
|
23
23
|
import { generateClickCode } from './clickCode.js';
|
|
24
24
|
import { emitJson, printOutputSchema } from './outputSchemas.js';
|
|
25
25
|
const COMMON_APP_PORTS = [3000, 3001, 4000, 5173, 5174, 8000, 8080];
|
|
@@ -368,55 +368,23 @@ async function checkSweetlinkAlive(wsUrl) {
|
|
|
368
368
|
* @param timeout Maximum time to wait in ms
|
|
369
369
|
* @returns true if server is ready, throws if timeout
|
|
370
370
|
*/
|
|
371
|
-
/**
|
|
372
|
-
* HEAD a local HTTPS URL via node:https with TLS verification relaxed, resolving
|
|
373
|
-
* its status code. Only used for local hosts (`isLocalHost`): their self-signed
|
|
374
|
-
* certs (Portless `*.localhost` on :443, mkcert) otherwise reject the health
|
|
375
|
-
* check before the server is ever reached, so a portless dev server looks "not
|
|
376
|
-
* ready" even though it is serving. `fetch` has no ergonomic per-request cert
|
|
377
|
-
* bypass, hence node:https here.
|
|
378
|
-
*/
|
|
379
|
-
function headStatusInsecureLocal(target, timeoutMs) {
|
|
380
|
-
return new Promise((resolve, reject) => {
|
|
381
|
-
const req = https.request(target, { method: 'HEAD', rejectUnauthorized: false }, (res) => {
|
|
382
|
-
res.resume(); // drain so the socket can close
|
|
383
|
-
resolve(res.statusCode ?? 0);
|
|
384
|
-
});
|
|
385
|
-
req.setTimeout(timeoutMs, () => req.destroy(new Error('timeout')));
|
|
386
|
-
req.on('error', reject);
|
|
387
|
-
req.end();
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
371
|
async function waitForServer(url, timeout = SERVER_READY_TIMEOUT) {
|
|
391
372
|
const startTime = Date.now();
|
|
392
373
|
let lastError = null;
|
|
393
374
|
// Parse URL to get just the origin for health check
|
|
394
375
|
const parsedUrl = new URL(url);
|
|
395
376
|
const healthCheckUrl = `${parsedUrl.protocol}//${parsedUrl.host}`;
|
|
396
|
-
// Local HTTPS dev servers (Portless `*.localhost`, mkcert) serve self-signed
|
|
397
|
-
// certs that fetch's TLS layer rejects; probe those via node:https with
|
|
398
|
-
// verification relaxed. Everything else keeps the plain fetch health check.
|
|
399
|
-
const relaxTls = parsedUrl.protocol === 'https:' && isLocalHost(parsedUrl.hostname);
|
|
400
377
|
console.log(`[Sweetlink] Waiting for server at ${healthCheckUrl}...`);
|
|
401
378
|
while (Date.now() - startTime < timeout) {
|
|
402
379
|
try {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
412
|
-
const response = await fetch(healthCheckUrl, {
|
|
413
|
-
method: 'HEAD',
|
|
414
|
-
signal: controller.signal,
|
|
415
|
-
});
|
|
416
|
-
clearTimeout(timeoutId);
|
|
417
|
-
ok = response.ok || response.status === 304;
|
|
418
|
-
}
|
|
419
|
-
if (ok) {
|
|
380
|
+
const controller = new AbortController();
|
|
381
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000);
|
|
382
|
+
const response = await fetch(healthCheckUrl, {
|
|
383
|
+
method: 'HEAD',
|
|
384
|
+
signal: controller.signal,
|
|
385
|
+
});
|
|
386
|
+
clearTimeout(timeoutId);
|
|
387
|
+
if (response.ok || response.status === 304) {
|
|
420
388
|
console.log(`[Sweetlink] Server ready (${Date.now() - startTime}ms)`);
|
|
421
389
|
return true;
|
|
422
390
|
}
|
|
@@ -630,13 +598,65 @@ async function takePlaywrightScreenshot(options, method) {
|
|
|
630
598
|
...(options.hideDevbar ? { devbarHidden: true } : {}),
|
|
631
599
|
};
|
|
632
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Screenshot when no browser client is connected, preferring the persistent daemon.
|
|
603
|
+
*
|
|
604
|
+
* `takePlaywrightScreenshot` cold-launches a browser per invocation (~2s). That is
|
|
605
|
+
* fine for a one-off, but the automated callers — visual-evidence gallery frames,
|
|
606
|
+
* batch capture runs — take dozens of shots in a row and pay it every single time.
|
|
607
|
+
* They never pass `--hifi`, so they never reached the persistent daemon that exists
|
|
608
|
+
* for exactly this case and serves repeat shots in ~150ms.
|
|
609
|
+
*
|
|
610
|
+
* Worse, the daemon was unreachable precisely where it helps most: the routes those
|
|
611
|
+
* captures target render without devbar, so there is never a connected browser
|
|
612
|
+
* client and the WebSocket path always escalates here.
|
|
613
|
+
*
|
|
614
|
+
* So make the warm path the default rather than an opt-in flag, and fall back to a
|
|
615
|
+
* standalone launch whenever the daemon can't serve the request. The returned
|
|
616
|
+
* `method` names the engine that actually served the frame, so a silent regression
|
|
617
|
+
* back to per-frame launches is visible in the log rather than invisible.
|
|
618
|
+
*/
|
|
619
|
+
async function takeScreenshotPreferDaemon(options, targetUrl, fallbackMethod) {
|
|
620
|
+
// --hover is a standalone-Playwright capability; the daemon screenshot request
|
|
621
|
+
// has no hover support, so don't route those through it.
|
|
622
|
+
if (!options.hover) {
|
|
623
|
+
try {
|
|
624
|
+
const daemonState = await ensureDaemon(findProjectRoot(), targetUrl);
|
|
625
|
+
const resp = await daemonRequest(daemonState, 'screenshot', {
|
|
626
|
+
selector: options.selector,
|
|
627
|
+
fullPage: options.fullPage,
|
|
628
|
+
viewport: options.viewport,
|
|
629
|
+
hideDevbar: options.hideDevbar,
|
|
630
|
+
url: options.url || targetUrl,
|
|
631
|
+
});
|
|
632
|
+
const data = resp.data;
|
|
633
|
+
const outputPath = options.output || getDefaultScreenshotPath();
|
|
634
|
+
ensureDir(outputPath);
|
|
635
|
+
fs.writeFileSync(outputPath, Buffer.from(data.screenshot, 'base64'));
|
|
636
|
+
const method = 'Daemon (auto-escalation)';
|
|
637
|
+
reportScreenshotSuccess(outputPath, data.width, data.height, method, options.selector);
|
|
638
|
+
return {
|
|
639
|
+
path: getRelativePath(outputPath),
|
|
640
|
+
width: data.width,
|
|
641
|
+
height: data.height,
|
|
642
|
+
method,
|
|
643
|
+
selector: options.selector,
|
|
644
|
+
...(options.hideDevbar ? { devbarHidden: true } : {}),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
console.warn(`[Sweetlink] Daemon unavailable (${error instanceof Error ? error.message : error}) — falling back to a standalone browser`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return await takePlaywrightScreenshot(options, fallbackMethod);
|
|
652
|
+
}
|
|
633
653
|
async function screenshot(options) {
|
|
634
654
|
// Convert --width/--height to viewport format if provided
|
|
635
655
|
if (options.width && !options.viewport) {
|
|
636
656
|
const height = options.height || Math.round(options.width * 1.5); // Default aspect ratio
|
|
637
657
|
options.viewport = `${options.width}x${height}`;
|
|
638
658
|
}
|
|
639
|
-
const targetUrl = options.url ||
|
|
659
|
+
const targetUrl = options.url || defaultDevUrl();
|
|
640
660
|
// Auto-wait for server if --wait flag is set or if URL is provided
|
|
641
661
|
// This eliminates the need for external sleep workarounds
|
|
642
662
|
if (options.wait !== false) {
|
|
@@ -734,7 +754,11 @@ async function screenshot(options) {
|
|
|
734
754
|
// This prevents launching a headless browser only to discover the dev server is dead.
|
|
735
755
|
const serverAlive = await checkSweetlinkAlive();
|
|
736
756
|
if (!serverAlive && !options.forceCDP) {
|
|
737
|
-
|
|
757
|
+
// Not necessarily a fault: pages rendered without devbar (isolated component
|
|
758
|
+
// render routes, production builds) never host a Sweetlink server. Say what
|
|
759
|
+
// happens next rather than implying a standalone launch is the only option —
|
|
760
|
+
// the escalation path prefers the persistent daemon.
|
|
761
|
+
console.warn('[Sweetlink] No Sweetlink server on the page — capturing via browser automation');
|
|
738
762
|
}
|
|
739
763
|
// Check if CDP is available (unless force WS is specified)
|
|
740
764
|
// Hover requires CDP/Playwright
|
|
@@ -774,9 +798,10 @@ async function screenshot(options) {
|
|
|
774
798
|
console.error('[Sweetlink] Could not navigate browser to', options.url);
|
|
775
799
|
process.exit(1);
|
|
776
800
|
}
|
|
777
|
-
// Auto-escalate
|
|
778
|
-
|
|
779
|
-
|
|
801
|
+
// Auto-escalate (opens browser, navigates, screenshots), preferring the
|
|
802
|
+
// persistent daemon so a batch of frames doesn't relaunch a browser each time.
|
|
803
|
+
console.log('[Sweetlink] No browser for navigation — escalating');
|
|
804
|
+
return await takeScreenshotPreferDaemon(playwrightOpts, targetUrl, 'Playwright (auto-escalation)');
|
|
780
805
|
}
|
|
781
806
|
}
|
|
782
807
|
console.log('[Sweetlink] Using WebSocket for screenshot');
|
|
@@ -796,9 +821,9 @@ async function screenshot(options) {
|
|
|
796
821
|
// Auto-escalate to Playwright when no browser client is connected
|
|
797
822
|
// This happens after dev server restart when browser page hasn't been refreshed
|
|
798
823
|
if (response.error?.includes('No browser client connected')) {
|
|
799
|
-
console.log('[Sweetlink] No browser client - auto-escalating
|
|
824
|
+
console.log('[Sweetlink] No browser client - auto-escalating');
|
|
800
825
|
try {
|
|
801
|
-
return await
|
|
826
|
+
return await takeScreenshotPreferDaemon(playwrightOpts, targetUrl, 'Playwright (auto-escalation)');
|
|
802
827
|
}
|
|
803
828
|
catch (playwrightError) {
|
|
804
829
|
console.error('[Sweetlink] Playwright fallback also failed:', playwrightError instanceof Error ? playwrightError.message : playwrightError);
|
|
@@ -844,6 +869,90 @@ async function screenshot(options) {
|
|
|
844
869
|
process.exit(1);
|
|
845
870
|
}
|
|
846
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* Capture many frames in one process against one browser.
|
|
874
|
+
*
|
|
875
|
+
* Even with the daemon serving frames, driving a batch as N separate CLI
|
|
876
|
+
* invocations pays Node startup and module load per frame — which dominates once
|
|
877
|
+
* the browser launch is gone. Callers that already know their whole frame list
|
|
878
|
+
* (visual-evidence capture plans, gallery sweeps) can hand it over once and get
|
|
879
|
+
* a single process, a single daemon handshake, and a single server-ready wait.
|
|
880
|
+
*
|
|
881
|
+
* One frame failing does not abort the batch: a capture plan is more useful with
|
|
882
|
+
* 25 of 26 frames plus a named failure than with nothing, and the caller decides
|
|
883
|
+
* whether a partial set is acceptable.
|
|
884
|
+
*/
|
|
885
|
+
async function screenshotBatch(options) {
|
|
886
|
+
const { items, defaults } = options;
|
|
887
|
+
const firstUrl = items[0].url;
|
|
888
|
+
if (options.wait !== false) {
|
|
889
|
+
try {
|
|
890
|
+
await waitForServer(firstUrl, options.waitTimeout || SERVER_READY_TIMEOUT);
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
console.error('[Sweetlink] Server not available:', error instanceof Error ? error.message : error);
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
const resolve = (item) => ({
|
|
898
|
+
selector: item.selector ?? defaults.selector,
|
|
899
|
+
fullPage: item.fullPage ?? defaults.fullPage,
|
|
900
|
+
viewport: item.viewport ?? defaults.viewport,
|
|
901
|
+
hideDevbar: item.hideDevbar ?? defaults.hideDevbar,
|
|
902
|
+
});
|
|
903
|
+
// One handshake for the whole batch. If the daemon can't start we still run
|
|
904
|
+
// every frame in this process — slower per frame, but the caller keeps the
|
|
905
|
+
// single-invocation win instead of the batch failing outright.
|
|
906
|
+
let daemonState = null;
|
|
907
|
+
try {
|
|
908
|
+
daemonState = await ensureDaemon(findProjectRoot(), firstUrl);
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
console.warn(`[Sweetlink] Daemon unavailable (${error instanceof Error ? error.message : error}) — running the batch against standalone browsers`);
|
|
912
|
+
}
|
|
913
|
+
const engine = daemonState ? 'Daemon (batch)' : 'Playwright (batch fallback)';
|
|
914
|
+
const captured = [];
|
|
915
|
+
for (const [index, item] of items.entries()) {
|
|
916
|
+
const shared = resolve(item);
|
|
917
|
+
try {
|
|
918
|
+
if (daemonState) {
|
|
919
|
+
const resp = await daemonRequest(daemonState, 'screenshot', {
|
|
920
|
+
...shared,
|
|
921
|
+
url: item.url,
|
|
922
|
+
});
|
|
923
|
+
const data = resp.data;
|
|
924
|
+
ensureDir(item.output);
|
|
925
|
+
fs.writeFileSync(item.output, Buffer.from(data.screenshot, 'base64'));
|
|
926
|
+
captured.push({
|
|
927
|
+
url: item.url,
|
|
928
|
+
output: getRelativePath(item.output),
|
|
929
|
+
ok: true,
|
|
930
|
+
width: data.width,
|
|
931
|
+
height: data.height,
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
const result = await takePlaywrightScreenshot({ ...shared, output: item.output, url: item.url }, 'Playwright (batch fallback)');
|
|
936
|
+
captured.push({
|
|
937
|
+
url: item.url,
|
|
938
|
+
output: result.path,
|
|
939
|
+
ok: true,
|
|
940
|
+
width: result.width,
|
|
941
|
+
height: result.height,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
console.log(`[Sweetlink] [${index + 1}/${items.length}] ${item.url}`);
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
948
|
+
captured.push({ url: item.url, output: item.output, ok: false, error: message });
|
|
949
|
+
console.warn(`[Sweetlink] [${index + 1}/${items.length}] FAILED ${item.url} — ${message}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const failed = captured.filter((c) => !c.ok).length;
|
|
953
|
+
console.log(`[Sweetlink] ✓ Batch complete: ${captured.length - failed}/${captured.length} frames via ${engine}`);
|
|
954
|
+
return { engine, captured, failed };
|
|
955
|
+
}
|
|
847
956
|
async function queryDOM(options) {
|
|
848
957
|
// Wait for selector if requested (handles hydration timing)
|
|
849
958
|
if (options.waitFor) {
|
|
@@ -1098,7 +1207,7 @@ async function execViaPlaywright(code) {
|
|
|
1098
1207
|
let page;
|
|
1099
1208
|
if (contexts.length > 0) {
|
|
1100
1209
|
const pages = contexts[0].pages();
|
|
1101
|
-
const devUrl = new URL(
|
|
1210
|
+
const devUrl = new URL(defaultDevUrl());
|
|
1102
1211
|
const devHost = devUrl.hostname;
|
|
1103
1212
|
const devPort = devUrl.port || (devUrl.protocol === 'https:' ? '443' : '80');
|
|
1104
1213
|
page = pages.find((p) => p.url().includes(`${devHost}:${devPort}`) || p.url().includes(`127.0.0.1:${devPort}`));
|
|
@@ -1109,7 +1218,7 @@ async function execViaPlaywright(code) {
|
|
|
1109
1218
|
if (!page) {
|
|
1110
1219
|
const context = contexts[0] || (await browser.newContext());
|
|
1111
1220
|
page = await context.newPage();
|
|
1112
|
-
await page.goto(
|
|
1221
|
+
await page.goto(defaultDevUrl(), {
|
|
1113
1222
|
waitUntil: 'domcontentloaded',
|
|
1114
1223
|
});
|
|
1115
1224
|
}
|
|
@@ -1808,8 +1917,16 @@ const COMMAND_HELP = {
|
|
|
1808
1917
|
- Auto-launches headless browser if no browser connected
|
|
1809
1918
|
|
|
1810
1919
|
Options:
|
|
1811
|
-
--url <url> Navigate browser to URL before capturing
|
|
1920
|
+
--url <url> Navigate browser to URL before capturing
|
|
1921
|
+
(default: $SWEETLINK_DEV_URL, else $PORTLESS_URL, else http://localhost:3000)
|
|
1812
1922
|
Navigates connected browser via WS; if none connected, opens one via Playwright
|
|
1923
|
+
--batch <path|-> Capture many frames in ONE process from a JSON manifest
|
|
1924
|
+
('-' reads stdin). Array of { url, output, selector?,
|
|
1925
|
+
fullPage?, viewport?, hideDevbar? }; per-item values win,
|
|
1926
|
+
the other flags here supply defaults. One server wait and
|
|
1927
|
+
one daemon handshake for the whole set — use this instead
|
|
1928
|
+
of a shell loop, which pays Node startup per frame.
|
|
1929
|
+
A failed frame is reported and the batch continues.
|
|
1813
1930
|
--selector <css-selector> CSS selector of element to screenshot
|
|
1814
1931
|
--output <path> Output file path (default: screenshot-<timestamp>.png)
|
|
1815
1932
|
--full-page Capture full scrollable page (default: viewport only)
|
|
@@ -2481,7 +2598,7 @@ async function runOneBatchCapture(cap) {
|
|
|
2481
2598
|
});
|
|
2482
2599
|
}
|
|
2483
2600
|
if (mode === 'screenshot') {
|
|
2484
|
-
const targetUrl = cap.url ??
|
|
2601
|
+
const targetUrl = cap.url ?? defaultDevUrl();
|
|
2485
2602
|
const projRoot = findProjectRoot();
|
|
2486
2603
|
const state = await ensureDaemon(projRoot, targetUrl);
|
|
2487
2604
|
const resp = await daemonRequest(state, 'screenshot', {
|
|
@@ -2625,7 +2742,7 @@ function printErrorContext(error) {
|
|
|
2625
2742
|
* Handle the `wait` command: wait for a server to be ready.
|
|
2626
2743
|
*/
|
|
2627
2744
|
async function handleWaitCommand() {
|
|
2628
|
-
const waitUrl = getArg('--url') ||
|
|
2745
|
+
const waitUrl = getArg('--url') || defaultDevUrl();
|
|
2629
2746
|
const waitTimeout = getArg('--timeout')
|
|
2630
2747
|
? parseInt(getArg('--timeout'), 10)
|
|
2631
2748
|
: SERVER_READY_TIMEOUT;
|
|
@@ -2644,7 +2761,7 @@ async function handleWaitCommand() {
|
|
|
2644
2761
|
* Handle the `status` command: quick non-blocking server health check.
|
|
2645
2762
|
*/
|
|
2646
2763
|
async function handleStatusCommand() {
|
|
2647
|
-
const statusUrl = getArg('--url') ||
|
|
2764
|
+
const statusUrl = getArg('--url') || defaultDevUrl();
|
|
2648
2765
|
try {
|
|
2649
2766
|
const parsedUrl = new URL(statusUrl);
|
|
2650
2767
|
const healthCheckUrl = `${parsedUrl.protocol}//${parsedUrl.host}`;
|
|
@@ -2670,6 +2787,25 @@ async function handleStatusCommand() {
|
|
|
2670
2787
|
}
|
|
2671
2788
|
}
|
|
2672
2789
|
async function handleScreenshotCmd() {
|
|
2790
|
+
// --batch takes a manifest of frames and captures them all in this one
|
|
2791
|
+
// process. Per-frame flags in the manifest win; the top-level flags below
|
|
2792
|
+
// supply the defaults, so `--hide-devbar --full-page --batch plan.json`
|
|
2793
|
+
// behaves like the same flags repeated on every item.
|
|
2794
|
+
const batchPath = getArg('--batch');
|
|
2795
|
+
if (batchPath) {
|
|
2796
|
+
const raw = batchPath === '-' ? fs.readFileSync(0, 'utf-8') : fs.readFileSync(batchPath, 'utf-8');
|
|
2797
|
+
return screenshotBatch({
|
|
2798
|
+
items: parseBatchManifest(raw),
|
|
2799
|
+
defaults: {
|
|
2800
|
+
fullPage: hasFlag('--full-page'),
|
|
2801
|
+
hideDevbar: hasFlag('--hide-devbar'),
|
|
2802
|
+
viewport: getArg('--viewport'),
|
|
2803
|
+
selector: getArg('--selector'),
|
|
2804
|
+
},
|
|
2805
|
+
wait: !hasFlag('--no-wait'),
|
|
2806
|
+
waitTimeout: getArg('--wait-timeout') ? parseInt(getArg('--wait-timeout'), 10) : undefined,
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2673
2809
|
return screenshot({
|
|
2674
2810
|
selector: getArg('--selector'),
|
|
2675
2811
|
output: getArg('--output'),
|
|
@@ -2693,7 +2829,7 @@ async function handleScreenshotCmd() {
|
|
|
2693
2829
|
}
|
|
2694
2830
|
async function handleInspectCmd() {
|
|
2695
2831
|
const projRoot = findProjectRoot();
|
|
2696
|
-
const targetUrl = getArg('--url') ??
|
|
2832
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
2697
2833
|
const state = await ensureDaemon(projRoot, targetUrl);
|
|
2698
2834
|
const actionTranscript = [];
|
|
2699
2835
|
args.forEach((arg, index) => {
|
|
@@ -2781,7 +2917,7 @@ async function handleClickCmd() {
|
|
|
2781
2917
|
const clickText = getArg('--text');
|
|
2782
2918
|
const clickIndex = getArg('--index') ? parseInt(getArg('--index'), 10) : 0;
|
|
2783
2919
|
const projRoot = findProjectRoot();
|
|
2784
|
-
const targetUrl = getArg('--url') ??
|
|
2920
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
2785
2921
|
// Route @e refs to daemon
|
|
2786
2922
|
if (clickTarget && /^@e\d+$/.test(clickTarget)) {
|
|
2787
2923
|
const state = await ensureDaemon(projRoot, targetUrl);
|
|
@@ -2826,7 +2962,7 @@ async function handleClickCmd() {
|
|
|
2826
2962
|
async function handleNetworkCmd() {
|
|
2827
2963
|
if (hasFlag('--failed')) {
|
|
2828
2964
|
const projRoot = findProjectRoot();
|
|
2829
|
-
const targetUrl = getArg('--url') ??
|
|
2965
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
2830
2966
|
const lastN = getArg('--last') ? parseInt(getArg('--last'), 10) : undefined;
|
|
2831
2967
|
const state = await ensureDaemon(projRoot, targetUrl);
|
|
2832
2968
|
const resp = await daemonRequest(state, 'network-read', {
|
|
@@ -2896,7 +3032,7 @@ async function handleSetupCmd() {
|
|
|
2896
3032
|
}
|
|
2897
3033
|
async function handleConsoleCmd() {
|
|
2898
3034
|
const projRoot = findProjectRoot();
|
|
2899
|
-
const targetUrl = getArg('--url') ??
|
|
3035
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
2900
3036
|
const errorsOnly = hasFlag('--errors');
|
|
2901
3037
|
const lastN = getArg('--last') ? parseInt(getArg('--last'), 10) : undefined;
|
|
2902
3038
|
const state = await ensureDaemon(projRoot, targetUrl);
|
|
@@ -2912,7 +3048,7 @@ async function handleConsoleCmd() {
|
|
|
2912
3048
|
async function handleSessionsCmd() {
|
|
2913
3049
|
const sub = args[1];
|
|
2914
3050
|
const projRoot = findProjectRoot();
|
|
2915
|
-
const targetUrl = getArg('--url') ??
|
|
3051
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
2916
3052
|
const state = await ensureDaemon(projRoot, targetUrl);
|
|
2917
3053
|
const resp = await daemonRequest(state, 'sessions-list');
|
|
2918
3054
|
const data = resp.data;
|
|
@@ -3047,7 +3183,7 @@ async function handleDemoCmd() {
|
|
|
3047
3183
|
return { sections: updated.sections.length, exitCode: lastSection.exitCode };
|
|
3048
3184
|
}
|
|
3049
3185
|
if (sub === 'screenshot') {
|
|
3050
|
-
const targetUrl = getArg('--url') ??
|
|
3186
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
3051
3187
|
const caption = getArg('--caption') ?? 'Screenshot';
|
|
3052
3188
|
const daemonState = await ensureDaemon(projRoot, targetUrl);
|
|
3053
3189
|
const resp = await daemonRequest(daemonState, 'screenshot', {});
|
|
@@ -3059,7 +3195,7 @@ async function handleDemoCmd() {
|
|
|
3059
3195
|
return { sections: updated.sections.length };
|
|
3060
3196
|
}
|
|
3061
3197
|
if (sub === 'snapshot') {
|
|
3062
|
-
const targetUrl = getArg('--url') ??
|
|
3198
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
3063
3199
|
const daemonState = await ensureDaemon(projRoot, targetUrl);
|
|
3064
3200
|
const resp = await daemonRequest(daemonState, 'snapshot', { interactive: true });
|
|
3065
3201
|
const data = resp.data;
|
|
@@ -3119,7 +3255,7 @@ async function handleDaemonCmd() {
|
|
|
3119
3255
|
// so honour --url for status/stop too — otherwise they look up the
|
|
3120
3256
|
// un-suffixed `daemon.json` and miss the daemon that `start`
|
|
3121
3257
|
// wrote with --url.
|
|
3122
|
-
const targetUrl = getArg('--url') ??
|
|
3258
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
3123
3259
|
const appPort = extractPort(targetUrl);
|
|
3124
3260
|
if (subcommand === 'stop') {
|
|
3125
3261
|
const stopped = await stopDaemon(projRoot, appPort);
|
|
@@ -3160,7 +3296,7 @@ async function handleFillCmd() {
|
|
|
3160
3296
|
}
|
|
3161
3297
|
if (/^@e\d+$/.test(fillTarget)) {
|
|
3162
3298
|
const projRoot = findProjectRoot();
|
|
3163
|
-
const targetUrl = getArg('--url') ??
|
|
3299
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
3164
3300
|
const state = await ensureDaemon(projRoot, targetUrl);
|
|
3165
3301
|
await daemonRequest(state, 'fill-ref', { ref: fillTarget, value: fillValue });
|
|
3166
3302
|
console.log(`[Sweetlink] Filled ${fillTarget} with "${fillValue}"`);
|
|
@@ -3171,7 +3307,7 @@ async function handleFillCmd() {
|
|
|
3171
3307
|
}
|
|
3172
3308
|
async function handleSnapshotCmd() {
|
|
3173
3309
|
const projRoot = findProjectRoot();
|
|
3174
|
-
const targetUrl = getArg('--url') ??
|
|
3310
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
3175
3311
|
const interactive = hasFlag('-i') || hasFlag('--interactive');
|
|
3176
3312
|
const doDiff = hasFlag('-D') || hasFlag('--diff');
|
|
3177
3313
|
const doAnnotate = hasFlag('-a') || hasFlag('--annotate');
|
|
@@ -3394,9 +3530,7 @@ async function handleSimCmd() {
|
|
|
3394
3530
|
console.log(`[Sweetlink] ${recResult.recordingClosed ? '✓' : '⚠'} ${getRelativePath(output)} · ` +
|
|
3395
3531
|
`${recResult.durationSec.toFixed(1)}s · ${sizeKb}KB · ${recResult.device} · exit=${recResult.exitCode}` +
|
|
3396
3532
|
tapSuffix +
|
|
3397
|
-
(recResult.recordingClosed
|
|
3398
|
-
? ''
|
|
3399
|
-
: ' (recording was force-killed; mp4 may be incomplete)'));
|
|
3533
|
+
(recResult.recordingClosed ? '' : ' (recording was force-killed; mp4 may be incomplete)'));
|
|
3400
3534
|
const result = {
|
|
3401
3535
|
path: output,
|
|
3402
3536
|
device: recResult.device,
|
|
@@ -3487,7 +3621,7 @@ async function handleTermCmd() {
|
|
|
3487
3621
|
}
|
|
3488
3622
|
async function handleRecordCmd() {
|
|
3489
3623
|
const projRoot = findProjectRoot();
|
|
3490
|
-
const targetUrl = getArg('--url') ??
|
|
3624
|
+
const targetUrl = getArg('--url') ?? defaultDevUrl();
|
|
3491
3625
|
const subcommand = args[1];
|
|
3492
3626
|
const state = await ensureDaemon(projRoot, targetUrl);
|
|
3493
3627
|
if (subcommand === 'start') {
|
|
@@ -3505,8 +3639,7 @@ async function handleRecordCmd() {
|
|
|
3505
3639
|
params.trace = true;
|
|
3506
3640
|
const resp = await daemonRequest(state, 'record-start', params);
|
|
3507
3641
|
const data = resp.data;
|
|
3508
|
-
console.log(`[Sweetlink] Recording started: ${data.sessionId}`
|
|
3509
|
-
(data.label ? ` (${data.label})` : ''));
|
|
3642
|
+
console.log(`[Sweetlink] Recording started: ${data.sessionId}${data.label ? ` (${data.label})` : ''}`);
|
|
3510
3643
|
return data;
|
|
3511
3644
|
}
|
|
3512
3645
|
if (subcommand === 'stop') {
|