@crouton-kit/capture 1.4.16 → 1.4.18

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 CHANGED
@@ -6010,11 +6010,12 @@ function normalizeUrlForMatch(value) {
6010
6010
  try {
6011
6011
  const url = new URL(value);
6012
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" : "");
6013
6016
  return {
6014
6017
  full: `${url.origin}${path48}${url.search}${url.hash}`.toLowerCase(),
6015
- // `host` (not `hostname`): two local dev servers on the same loopback
6016
- // address but different ports must not register as a host match.
6017
- host: url.host.toLowerCase(),
6018
+ origin: `${url.protocol}//${loopback ? "loopback" : hostname}:${port}`,
6018
6019
  path: path48.toLowerCase()
6019
6020
  };
6020
6021
  } catch {
@@ -6026,14 +6027,11 @@ function scoreTabUrlMatch(tabUrl, requestedUrl) {
6026
6027
  const requested = normalizeUrlForMatch(requestedUrl);
6027
6028
  if (tab && requested) {
6028
6029
  if (tab.full === requested.full) return 100;
6029
- if (tab.host === requested.host && tab.path === requested.path) return 95;
6030
- if (tab.host === requested.host && tab.path.startsWith(requested.path)) return 90;
6031
- if (tab.host === requested.host && requested.path.startsWith(tab.path)) return 85;
6032
- if (tab.host === requested.host) return 70;
6033
- if (tab.full.includes(requested.full)) return 60;
6034
- if (tab.full.includes(requested.host)) return 50;
6035
- if (requested.full.includes(tab.full)) return 40;
6036
- 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;
6037
6035
  }
6038
6036
  const tabLower = tabUrl.toLowerCase();
6039
6037
  const requestedLower = requestedUrl.toLowerCase();
@@ -10561,7 +10559,7 @@ function validateHarAppendBatch(value, source) {
10561
10559
  return { entries, incompleteLifecycles };
10562
10560
  }
10563
10561
  function captureVersion() {
10564
- const version = "1.4.16";
10562
+ const version = "1.4.18";
10565
10563
  return typeof version === "string" && version.length > 0 ? version : "development";
10566
10564
  }
10567
10565
  function token() {
@@ -12663,7 +12661,7 @@ function wallTime(value) {
12663
12661
  }
12664
12662
  }
12665
12663
  function captureVersion2() {
12666
- const version = "1.4.16";
12664
+ const version = "1.4.18";
12667
12665
  return typeof version === "string" && version.length > 0 ? version : "development";
12668
12666
  }
12669
12667
  function deepFreeze(value) {
@@ -231612,7 +231610,11 @@ var init_core = __esm({
231612
231610
  walk2("");
231613
231611
  return files;
231614
231612
  }
231615
- async drainOne(live, cause) {
231613
+ drainOne(live, cause) {
231614
+ if (!live.draining) live.draining = this.drain(live, cause);
231615
+ return live.draining;
231616
+ }
231617
+ async drain(live, cause) {
231616
231618
  const outcome = await live.collector.drain(cause);
231617
231619
  unlinkPrivateFile(path24.join(live.row.dir, "collecting.json"));
231618
231620
  const files = this.finalizedFiles(live.row.dir);
@@ -231642,9 +231644,10 @@ var init_core = __esm({
231642
231644
  return { completion: drained.completion, summary: drained.outcome.summary, files: drained.outcome.files };
231643
231645
  }
231644
231646
  async teardown(trigger = "session-stop") {
231645
- const stopping = this.cutoff(this.live.values());
231647
+ const liveCollectors = [...this.live.values()];
231648
+ this.cutoff(liveCollectors);
231646
231649
  const outcomes = [];
231647
- for (const live of stopping) {
231650
+ for (const live of liveCollectors) {
231648
231651
  try {
231649
231652
  const drained = await this.drainOne(live, { trigger, clientUsable: this.clientUsable });
231650
231653
  outcomes.push({ status: "drained", id: live.row.id, kind: live.row.kind, completion: drained.completion, dir: live.row.dir });
@@ -244116,7 +244119,7 @@ async function stopComposedRecorder(opts) {
244116
244119
  const host = await liveHost(opts.sessionDir);
244117
244120
  const row = motionHandle(host, recId);
244118
244121
  if (!row) throw new Error(`No live motion collector named "${recId}".`);
244119
- const response = await sendHostRequest(host.socketPath, { type: "collector-stop", nonce: host.nonce, id: recId }, 3e4);
244122
+ const response = await sendHostRequest(host.socketPath, { type: "collector-stop", nonce: host.nonce, id: recId }, 12e4);
244120
244123
  if (!response.ok) throw new Error(response.error ?? "collector host refused to stop motion recording");
244121
244124
  if (getActiveSession()?.dir === opts.sessionDir && getActiveRecId() === recId) await clearActiveRecId();
244122
244125
  return finalFromMeta(row.dir, recId);
@@ -267359,7 +267362,7 @@ I/O contract: flags and positionals on input; one rendered prose block on stdout
267359
267362
  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.`;
267360
267363
  }
267361
267364
  function printVersion() {
267362
- const declared = "1.4.16";
267365
+ const declared = "1.4.18";
267363
267366
  if (declared) {
267364
267367
  console.log(declared);
267365
267368
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/capture",
3
- "version": "1.4.16",
3
+ "version": "1.4.18",
4
4
  "description": "Browser automation and UI validation via CDP — session-based screenshots, HAR, a11y, click/type, and JS exec",
5
5
  "bin": {
6
6
  "capture": "./bin/capture"
@@ -12,7 +12,7 @@ export interface CollectorRow { id: string; kind: CollectorKind; dir: string; cl
12
12
  export interface HostSnapshot { collectors: readonly CollectorRow[]; reservations: readonly ClaimReservation[]; }
13
13
  export interface HostIdentity { pid: number; birth: PidBirth; targetId: string; }
14
14
 
15
- interface LiveCollector { row: CollectorRow; collector: Collector; startedAt: string; losses: Array<{ reason: string; detail?: Record<string, unknown> }>; stopping: boolean; }
15
+ interface LiveCollector { row: CollectorRow; collector: Collector; startedAt: string; losses: Array<{ reason: string; detail?: Record<string, unknown> }>; stopping: boolean; draining?: Promise<{ completion: Completion; outcome: DrainOutcome }>; }
16
16
 
17
17
  export class CollectorHost {
18
18
  private readonly live = new Map<string, LiveCollector>();
@@ -131,7 +131,12 @@ export class CollectorHost {
131
131
  return files;
132
132
  }
133
133
 
134
- private async drainOne(live: LiveCollector, cause: DrainCause): Promise<{ completion: Completion; outcome: DrainOutcome }> {
134
+ private drainOne(live: LiveCollector, cause: DrainCause): Promise<{ completion: Completion; outcome: DrainOutcome }> {
135
+ if (!live.draining) live.draining = this.drain(live, cause);
136
+ return live.draining;
137
+ }
138
+
139
+ private async drain(live: LiveCollector, cause: DrainCause): Promise<{ completion: Completion; outcome: DrainOutcome }> {
135
140
  const outcome = await live.collector.drain(cause);
136
141
  unlinkPrivateFile(path.join(live.row.dir, 'collecting.json'));
137
142
  const files = this.finalizedFiles(live.row.dir);
@@ -163,9 +168,10 @@ export class CollectorHost {
163
168
  }
164
169
 
165
170
  async teardown(trigger: DrainCause['trigger'] = 'session-stop'): Promise<TeardownOutcome[]> {
166
- const stopping = this.cutoff(this.live.values());
171
+ const liveCollectors = [...this.live.values()];
172
+ this.cutoff(liveCollectors);
167
173
  const outcomes: TeardownOutcome[] = [];
168
- for (const live of stopping) {
174
+ for (const live of liveCollectors) {
169
175
  try {
170
176
  const drained = await this.drainOne(live, { trigger, clientUsable: this.clientUsable });
171
177
  outcomes.push({ status: 'drained', id: live.row.id, kind: live.row.kind, completion: drained.completion, dir: live.row.dir });
@@ -100,7 +100,7 @@ export async function stopComposedRecorder(opts: { sessionDir: string; recId?: s
100
100
  const host = await liveHost(opts.sessionDir);
101
101
  const row = motionHandle(host, recId);
102
102
  if (!row) throw new Error(`No live motion collector named "${recId}".`);
103
- const response = await sendHostRequest(host.socketPath, { type: 'collector-stop', nonce: host.nonce, id: recId }, 30_000);
103
+ const response = await sendHostRequest(host.socketPath, { type: 'collector-stop', nonce: host.nonce, id: recId }, 120_000);
104
104
  if (!response.ok) throw new Error(response.error ?? 'collector host refused to stop motion recording');
105
105
  if (getActiveSession()?.dir === opts.sessionDir && getActiveRecId() === recId) await clearActiveRecId();
106
106
  return finalFromMeta(row.dir, recId);
@@ -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; host: string; path: string } | null {
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
- // `host` (not `hostname`): two local dev servers on the same loopback
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.host === requested.host && tab.path === requested.path) return 95;
175
- if (tab.host === requested.host && tab.path.startsWith(requested.path)) return 90;
176
- if (tab.host === requested.host && requested.path.startsWith(tab.path)) return 85;
177
- if (tab.host === requested.host) return 70;
178
- if (tab.full.includes(requested.full)) return 60;
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();