@crouton-kit/capture 1.4.15 → 1.4.17
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/bin/capture +55 -26
- package/package.json +1 -1
- package/src/cdp/targets.ts +33 -12
- package/src/session/commands.ts +24 -12
package/bin/capture
CHANGED
|
@@ -5910,6 +5910,7 @@ var targets_exports = {};
|
|
|
5910
5910
|
__export(targets_exports, {
|
|
5911
5911
|
AmbiguousPageTargetError: () => AmbiguousPageTargetError,
|
|
5912
5912
|
closeTarget: () => closeTarget,
|
|
5913
|
+
findResponsivePageTabByUrl: () => findResponsivePageTabByUrl,
|
|
5913
5914
|
findTab: () => findTab,
|
|
5914
5915
|
findTabById: () => findTabById,
|
|
5915
5916
|
findTabByIdAcrossEndpoints: () => findTabByIdAcrossEndpoints,
|
|
@@ -6009,11 +6010,12 @@ function normalizeUrlForMatch(value) {
|
|
|
6009
6010
|
try {
|
|
6010
6011
|
const url = new URL(value);
|
|
6011
6012
|
const path48 = url.pathname.replace(/\/+$/, "") || "/";
|
|
6013
|
+
const hostname = url.hostname.toLowerCase();
|
|
6014
|
+
const loopback = hostname === "localhost" || hostname === "127.0.0.1";
|
|
6015
|
+
const port = url.port || (url.protocol === "http:" ? "80" : url.protocol === "https:" ? "443" : "");
|
|
6012
6016
|
return {
|
|
6013
6017
|
full: `${url.origin}${path48}${url.search}${url.hash}`.toLowerCase(),
|
|
6014
|
-
|
|
6015
|
-
// address but different ports must not register as a host match.
|
|
6016
|
-
host: url.host.toLowerCase(),
|
|
6018
|
+
origin: `${url.protocol}//${loopback ? "loopback" : hostname}:${port}`,
|
|
6017
6019
|
path: path48.toLowerCase()
|
|
6018
6020
|
};
|
|
6019
6021
|
} catch {
|
|
@@ -6025,14 +6027,11 @@ function scoreTabUrlMatch(tabUrl, requestedUrl) {
|
|
|
6025
6027
|
const requested = normalizeUrlForMatch(requestedUrl);
|
|
6026
6028
|
if (tab && requested) {
|
|
6027
6029
|
if (tab.full === requested.full) return 100;
|
|
6028
|
-
if (tab.
|
|
6029
|
-
if (tab.
|
|
6030
|
-
if (tab.
|
|
6031
|
-
if (tab.
|
|
6032
|
-
|
|
6033
|
-
if (tab.full.includes(requested.host)) return 50;
|
|
6034
|
-
if (requested.full.includes(tab.full)) return 40;
|
|
6035
|
-
return 0;
|
|
6030
|
+
if (tab.origin !== requested.origin) return 0;
|
|
6031
|
+
if (tab.path === requested.path) return 95;
|
|
6032
|
+
if (tab.path.startsWith(requested.path)) return 90;
|
|
6033
|
+
if (requested.path.startsWith(tab.path)) return 85;
|
|
6034
|
+
return 70;
|
|
6036
6035
|
}
|
|
6037
6036
|
const tabLower = tabUrl.toLowerCase();
|
|
6038
6037
|
const requestedLower = requestedUrl.toLowerCase();
|
|
@@ -6049,6 +6048,25 @@ async function findTabByIdAcrossEndpoints(targetId, preferredPort) {
|
|
|
6049
6048
|
const ports = resolveSearchPorts(await detectCdpPortsAsync(), preferredPort);
|
|
6050
6049
|
return findTabByIdInPorts(targetId, ports);
|
|
6051
6050
|
}
|
|
6051
|
+
async function findResponsivePageTabByUrl(port, url) {
|
|
6052
|
+
const pages = (await listTargets(port)).filter((target) => target.type === "page");
|
|
6053
|
+
const scores = pages.map((target) => scoreTabUrlMatch(target.url, url));
|
|
6054
|
+
const bestScore = Math.max(0, ...scores);
|
|
6055
|
+
if (bestScore === 0) return null;
|
|
6056
|
+
for (const target of pages.filter((_, index) => scores[index] === bestScore)) {
|
|
6057
|
+
if (!target.webSocketDebuggerUrl) continue;
|
|
6058
|
+
const client = new CDPClient(target.webSocketDebuggerUrl);
|
|
6059
|
+
try {
|
|
6060
|
+
await client.waitReady();
|
|
6061
|
+
await client.send("Page.captureScreenshot", { format: "png" }, 5e3);
|
|
6062
|
+
return target;
|
|
6063
|
+
} catch {
|
|
6064
|
+
} finally {
|
|
6065
|
+
client.close();
|
|
6066
|
+
}
|
|
6067
|
+
}
|
|
6068
|
+
return null;
|
|
6069
|
+
}
|
|
6052
6070
|
async function findTabByUrlAcrossEndpoints(url, preferredPort) {
|
|
6053
6071
|
const ports = resolveSearchPorts(await detectCdpPortsAsync(), preferredPort);
|
|
6054
6072
|
let best = null;
|
|
@@ -10541,7 +10559,7 @@ function validateHarAppendBatch(value, source) {
|
|
|
10541
10559
|
return { entries, incompleteLifecycles };
|
|
10542
10560
|
}
|
|
10543
10561
|
function captureVersion() {
|
|
10544
|
-
const version = "1.4.
|
|
10562
|
+
const version = "1.4.17";
|
|
10545
10563
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
10546
10564
|
}
|
|
10547
10565
|
function token() {
|
|
@@ -12643,7 +12661,7 @@ function wallTime(value) {
|
|
|
12643
12661
|
}
|
|
12644
12662
|
}
|
|
12645
12663
|
function captureVersion2() {
|
|
12646
|
-
const version = "1.4.
|
|
12664
|
+
const version = "1.4.17";
|
|
12647
12665
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
12648
12666
|
}
|
|
12649
12667
|
function deepFreeze(value) {
|
|
@@ -15140,6 +15158,7 @@ async function start(parsed) {
|
|
|
15140
15158
|
let bridgeSocket = null;
|
|
15141
15159
|
let bridgePid = null;
|
|
15142
15160
|
let cdpPort = null;
|
|
15161
|
+
let targetWasOpened = false;
|
|
15143
15162
|
let selectedBrowser = null;
|
|
15144
15163
|
let endpointSelectionReason = null;
|
|
15145
15164
|
try {
|
|
@@ -15166,22 +15185,28 @@ async function start(parsed) {
|
|
|
15166
15185
|
}
|
|
15167
15186
|
}
|
|
15168
15187
|
if (url) {
|
|
15188
|
+
let opened = false;
|
|
15169
15189
|
try {
|
|
15170
15190
|
target = await startWorld.openTab(cdpPort, url);
|
|
15191
|
+
opened = true;
|
|
15192
|
+
targetWasOpened = true;
|
|
15171
15193
|
} catch (error4) {
|
|
15172
15194
|
const message = error4 instanceof Error ? error4.message : String(error4);
|
|
15173
|
-
if (message.toLowerCase().includes("not supported"))
|
|
15195
|
+
if (!message.toLowerCase().includes("not supported")) throw error4;
|
|
15196
|
+
target = await startWorld.findResponsivePageTabByUrl?.(cdpPort, url) ?? null;
|
|
15197
|
+
if (!target) {
|
|
15174
15198
|
throw worldFailure(
|
|
15175
|
-
`Target.createTarget is unsupported on port ${cdpPort}
|
|
15199
|
+
`Target.createTarget is unsupported on port ${cdpPort}, and no matching existing page target answered a screenshot. Run \`capture tab list --port ${cdpPort}\` to inspect its targets.`,
|
|
15176
15200
|
error4
|
|
15177
15201
|
);
|
|
15178
15202
|
}
|
|
15179
|
-
throw error4;
|
|
15180
15203
|
}
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
|
|
15184
|
-
|
|
15204
|
+
if (opened) {
|
|
15205
|
+
const openedPort = cdpPort;
|
|
15206
|
+
const openedTargetId = target.id;
|
|
15207
|
+
acquired.push({ label: "opened target", release: () => startWorld.closeTarget(openedPort, openedTargetId) });
|
|
15208
|
+
pageLoadTimedOut = await startWorld.awaitTabReady(target, url);
|
|
15209
|
+
}
|
|
15185
15210
|
}
|
|
15186
15211
|
if (hold) {
|
|
15187
15212
|
const bridge = await startWorld.startBridge(dir, cdpPort);
|
|
@@ -15211,7 +15236,7 @@ async function start(parsed) {
|
|
|
15211
15236
|
return;
|
|
15212
15237
|
}
|
|
15213
15238
|
const rows = [fact`bundle dir: ${dir}`];
|
|
15214
|
-
if (target) rows.push(
|
|
15239
|
+
if (target) rows.push(targetWasOpened ? fact`tab ${target.id} opened at ${url}` : fact`tab ${target.id} adopted at ${target.url}`);
|
|
15215
15240
|
if (cdpPort !== null) rows.push(
|
|
15216
15241
|
parsed.portSource === "env" ? fact`CDP endpoint on port ${cdpPort} selected from ambient CDP_PORT; --port overrides endpoint selection and --target overrides tab selection.` : parsed.port !== void 0 ? fact`CDP endpoint on port ${cdpPort} selected by explicit --port; --port overrides endpoint selection and --target overrides tab selection.` : fact`${selectedBrowser ?? "CDP endpoint"} on port ${cdpPort} auto-selected because it ${endpointSelectionReason ?? "was selected by automatic endpoint detection"}; capture-launched browser recency does not affect selection. --port overrides endpoint selection and --target overrides tab selection.`
|
|
15217
15242
|
);
|
|
@@ -16128,10 +16153,10 @@ use when collecting or reading the traffic, logs, and artifacts of scoped work a
|
|
|
16128
16153
|
|
|
16129
16154
|
input:
|
|
16130
16155
|
--url <url> Absolute URL to open in a fresh tab through CDP's
|
|
16131
|
-
Target.createTarget.
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
--target.
|
|
16156
|
+
Target.createTarget. If an endpoint, including Electron,
|
|
16157
|
+
rejects Target.createTarget, capture adopts the matching
|
|
16158
|
+
existing page target that answers a screenshot. Conflicts
|
|
16159
|
+
with --target.
|
|
16135
16160
|
--target <tab-id> Existing page tab to adopt. Resolves a full target id or
|
|
16136
16161
|
an unambiguous prefix of at least four characters at the
|
|
16137
16162
|
selected CDP endpoint; start fails if it does not resolve.
|
|
@@ -16277,6 +16302,10 @@ effects:
|
|
|
16277
16302
|
const { findTabById: findTabById2 } = await Promise.resolve().then(() => (init_targets(), targets_exports));
|
|
16278
16303
|
return findTabById2(port, targetId);
|
|
16279
16304
|
},
|
|
16305
|
+
async findResponsivePageTabByUrl(port, url) {
|
|
16306
|
+
const { findResponsivePageTabByUrl: findResponsivePageTabByUrl2 } = await Promise.resolve().then(() => (init_targets(), targets_exports));
|
|
16307
|
+
return findResponsivePageTabByUrl2(port, url);
|
|
16308
|
+
},
|
|
16280
16309
|
async closeTarget(port, targetId) {
|
|
16281
16310
|
const { closeTarget: closeTarget2 } = await Promise.resolve().then(() => (init_targets(), targets_exports));
|
|
16282
16311
|
return closeTarget2(port, targetId);
|
|
@@ -267328,7 +267357,7 @@ I/O contract: flags and positionals on input; one rendered prose block on stdout
|
|
|
267328
267357
|
A CDP-enabled browser must be running; \`capture tab list\` is the probe, and \`capture tab launch\` starts one capture owns and reaps when nothing is (never hand-roll a detached browser \u2014 nothing reaps that). Capture never signals a browser it did not start, so attaching to your own with --port is unchanged. CDP_PORT / CDP_TARGET pin the browser + tab for target-selecting commands \u2014 precedence: explicit flag > active session > env. \`session start\` adopts only an explicit --target.`;
|
|
267329
267358
|
}
|
|
267330
267359
|
function printVersion() {
|
|
267331
|
-
const declared = "1.4.
|
|
267360
|
+
const declared = "1.4.17";
|
|
267332
267361
|
if (declared) {
|
|
267333
267362
|
console.log(declared);
|
|
267334
267363
|
return;
|
package/package.json
CHANGED
package/src/cdp/targets.ts
CHANGED
|
@@ -149,15 +149,17 @@ export async function findUnambiguousPageTabInPorts(
|
|
|
149
149
|
return candidates[0] ?? null;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function normalizeUrlForMatch(value: string): { full: string;
|
|
152
|
+
function normalizeUrlForMatch(value: string): { full: string; origin: string; path: string } | null {
|
|
153
153
|
try {
|
|
154
154
|
const url = new URL(value);
|
|
155
155
|
const path = url.pathname.replace(/\/+$/, '') || '/';
|
|
156
|
+
const hostname = url.hostname.toLowerCase();
|
|
157
|
+
// Electron may expose a localhost page when the caller uses its equivalent IPv4 loopback URL.
|
|
158
|
+
const loopback = hostname === 'localhost' || hostname === '127.0.0.1';
|
|
159
|
+
const port = url.port || (url.protocol === 'http:' ? '80' : url.protocol === 'https:' ? '443' : '');
|
|
156
160
|
return {
|
|
157
161
|
full: `${url.origin}${path}${url.search}${url.hash}`.toLowerCase(),
|
|
158
|
-
|
|
159
|
-
// address but different ports must not register as a host match.
|
|
160
|
-
host: url.host.toLowerCase(),
|
|
162
|
+
origin: `${url.protocol}//${loopback ? 'loopback' : hostname}:${port}`,
|
|
161
163
|
path: path.toLowerCase(),
|
|
162
164
|
};
|
|
163
165
|
} catch {
|
|
@@ -171,14 +173,11 @@ export function scoreTabUrlMatch(tabUrl: string, requestedUrl: string): number {
|
|
|
171
173
|
|
|
172
174
|
if (tab && requested) {
|
|
173
175
|
if (tab.full === requested.full) return 100;
|
|
174
|
-
if (tab.
|
|
175
|
-
if (tab.
|
|
176
|
-
if (tab.
|
|
177
|
-
if (tab.
|
|
178
|
-
|
|
179
|
-
if (tab.full.includes(requested.host)) return 50;
|
|
180
|
-
if (requested.full.includes(tab.full)) return 40;
|
|
181
|
-
return 0;
|
|
176
|
+
if (tab.origin !== requested.origin) return 0;
|
|
177
|
+
if (tab.path === requested.path) return 95;
|
|
178
|
+
if (tab.path.startsWith(requested.path)) return 90;
|
|
179
|
+
if (requested.path.startsWith(tab.path)) return 85;
|
|
180
|
+
return 70;
|
|
182
181
|
}
|
|
183
182
|
|
|
184
183
|
const tabLower = tabUrl.toLowerCase();
|
|
@@ -205,6 +204,28 @@ export async function findTabByIdAcrossEndpoints(
|
|
|
205
204
|
return findTabByIdInPorts(targetId, ports);
|
|
206
205
|
}
|
|
207
206
|
|
|
207
|
+
export async function findResponsivePageTabByUrl(port: number, url: string): Promise<CDPTarget | null> {
|
|
208
|
+
const pages = (await listTargets(port)).filter((target) => target.type === 'page');
|
|
209
|
+
const scores = pages.map((target) => scoreTabUrlMatch(target.url, url));
|
|
210
|
+
const bestScore = Math.max(0, ...scores);
|
|
211
|
+
if (bestScore === 0) return null;
|
|
212
|
+
|
|
213
|
+
for (const target of pages.filter((_, index) => scores[index] === bestScore)) {
|
|
214
|
+
if (!target.webSocketDebuggerUrl) continue;
|
|
215
|
+
const client = new CDPClient(target.webSocketDebuggerUrl);
|
|
216
|
+
try {
|
|
217
|
+
await client.waitReady();
|
|
218
|
+
await client.send('Page.captureScreenshot', { format: 'png' }, 5_000);
|
|
219
|
+
return target;
|
|
220
|
+
} catch {
|
|
221
|
+
// Electron can expose a page target that never answers screenshot requests.
|
|
222
|
+
} finally {
|
|
223
|
+
client.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
|
|
208
229
|
export async function findTabByUrlAcrossEndpoints(
|
|
209
230
|
url: string,
|
|
210
231
|
preferredPort?: number,
|
package/src/session/commands.ts
CHANGED
|
@@ -391,10 +391,10 @@ const START_USAGE = `capture session start [--url <url> | --target <tab-id>] [--
|
|
|
391
391
|
|
|
392
392
|
input:
|
|
393
393
|
--url <url> Absolute URL to open in a fresh tab through CDP's
|
|
394
|
-
Target.createTarget.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
--target.
|
|
394
|
+
Target.createTarget. If an endpoint, including Electron,
|
|
395
|
+
rejects Target.createTarget, capture adopts the matching
|
|
396
|
+
existing page target that answers a screenshot. Conflicts
|
|
397
|
+
with --target.
|
|
398
398
|
--target <tab-id> Existing page tab to adopt. Resolves a full target id or
|
|
399
399
|
an unambiguous prefix of at least four characters at the
|
|
400
400
|
selected CDP endpoint; start fails if it does not resolve.
|
|
@@ -563,6 +563,7 @@ export interface SessionStartWorld {
|
|
|
563
563
|
detectCdpPort(): Promise<number | { port: number; app: string; selectionReason: string }>;
|
|
564
564
|
openTab(port: number, url: string): Promise<CDPTarget>;
|
|
565
565
|
findTabById(port: number, targetId: string): Promise<CDPTarget | null>;
|
|
566
|
+
findResponsivePageTabByUrl?(port: number, url: string): Promise<CDPTarget | null>;
|
|
566
567
|
closeTarget(port: number, targetId: string): Promise<void>;
|
|
567
568
|
/** Attach to the freshly-opened tab and wait for load; returns page-load-timed-out. */
|
|
568
569
|
awaitTabReady(target: CDPTarget, url: string): Promise<boolean>;
|
|
@@ -588,6 +589,10 @@ const productionStartWorld: SessionStartWorld = {
|
|
|
588
589
|
const { findTabById } = await import('../cdp/targets.js');
|
|
589
590
|
return findTabById(port, targetId);
|
|
590
591
|
},
|
|
592
|
+
async findResponsivePageTabByUrl(port, url) {
|
|
593
|
+
const { findResponsivePageTabByUrl } = await import('../cdp/targets.js');
|
|
594
|
+
return findResponsivePageTabByUrl(port, url);
|
|
595
|
+
},
|
|
591
596
|
async closeTarget(port, targetId) {
|
|
592
597
|
const { closeTarget } = await import('../cdp/targets.js');
|
|
593
598
|
return closeTarget(port, targetId);
|
|
@@ -692,6 +697,7 @@ async function start(parsed: ParsedArgs): Promise<void> {
|
|
|
692
697
|
let bridgeSocket: string | null = null;
|
|
693
698
|
let bridgePid: number | null = null;
|
|
694
699
|
let cdpPort: number | null = null;
|
|
700
|
+
let targetWasOpened = false;
|
|
695
701
|
let selectedBrowser: string | null = null;
|
|
696
702
|
let endpointSelectionReason: string | null = null;
|
|
697
703
|
|
|
@@ -723,22 +729,28 @@ async function start(parsed: ParsedArgs): Promise<void> {
|
|
|
723
729
|
}
|
|
724
730
|
|
|
725
731
|
if (url) {
|
|
732
|
+
let opened = false;
|
|
726
733
|
try {
|
|
727
734
|
target = await startWorld.openTab(cdpPort!, url);
|
|
735
|
+
opened = true;
|
|
736
|
+
targetWasOpened = true;
|
|
728
737
|
} catch (error) {
|
|
729
738
|
const message = error instanceof Error ? error.message : String(error);
|
|
730
|
-
if (message.toLowerCase().includes('not supported'))
|
|
739
|
+
if (!message.toLowerCase().includes('not supported')) throw error;
|
|
740
|
+
target = await startWorld.findResponsivePageTabByUrl?.(cdpPort!, url) ?? null;
|
|
741
|
+
if (!target) {
|
|
731
742
|
throw worldFailure(
|
|
732
|
-
`Target.createTarget is unsupported on port ${cdpPort}
|
|
743
|
+
`Target.createTarget is unsupported on port ${cdpPort}, and no matching existing page target answered a screenshot. Run \`capture tab list --port ${cdpPort}\` to inspect its targets.`,
|
|
733
744
|
error,
|
|
734
745
|
);
|
|
735
746
|
}
|
|
736
|
-
throw error;
|
|
737
747
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
748
|
+
if (opened) {
|
|
749
|
+
const openedPort = cdpPort!;
|
|
750
|
+
const openedTargetId = target.id;
|
|
751
|
+
acquired.push({ label: 'opened target', release: () => startWorld.closeTarget(openedPort, openedTargetId) });
|
|
752
|
+
pageLoadTimedOut = await startWorld.awaitTabReady(target, url);
|
|
753
|
+
}
|
|
742
754
|
}
|
|
743
755
|
|
|
744
756
|
// Hold a CDP browser connection open for the session's lifetime so
|
|
@@ -778,7 +790,7 @@ async function start(parsed: ParsedArgs): Promise<void> {
|
|
|
778
790
|
}
|
|
779
791
|
|
|
780
792
|
const rows: FactLine[] = [fact`bundle dir: ${dir}`];
|
|
781
|
-
if (target) rows.push(
|
|
793
|
+
if (target) rows.push(targetWasOpened ? fact`tab ${target.id} opened at ${url}` : fact`tab ${target.id} adopted at ${target.url}`);
|
|
782
794
|
if (cdpPort !== null) rows.push(
|
|
783
795
|
parsed.portSource === 'env'
|
|
784
796
|
? fact`CDP endpoint on port ${cdpPort} selected from ambient CDP_PORT; --port overrides endpoint selection and --target overrides tab selection.`
|