@crouton-kit/capture 1.3.12 → 1.3.14

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
@@ -9822,7 +9822,7 @@ function validateHarAppendBatch(value, source) {
9822
9822
  return { entries, incompleteLifecycles };
9823
9823
  }
9824
9824
  function captureVersion() {
9825
- const version = "1.3.12";
9825
+ const version = "1.3.14";
9826
9826
  return typeof version === "string" && version.length > 0 ? version : "development";
9827
9827
  }
9828
9828
  function token() {
@@ -11581,7 +11581,7 @@ function wallTime(value) {
11581
11581
  }
11582
11582
  }
11583
11583
  function captureVersion2() {
11584
- const version = "1.3.12";
11584
+ const version = "1.3.14";
11585
11585
  return typeof version === "string" && version.length > 0 ? version : "development";
11586
11586
  }
11587
11587
  function deepFreeze(value) {
@@ -13842,9 +13842,9 @@ function readSession(id) {
13842
13842
  function generateId() {
13843
13843
  return `cap-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
13844
13844
  }
13845
- async function waitForPageLoad(client, timeoutMs) {
13845
+ async function waitForPageLoad(client, timeoutMs, attachTimeoutMs = SESSION_TAB_ATTACH_TIMEOUT_MS) {
13846
13846
  await client.waitReady();
13847
- await client.send("Page.enable");
13847
+ await client.send("Page.enable", {}, attachTimeoutMs);
13848
13848
  return await new Promise((resolve11) => {
13849
13849
  const timer = setTimeout(() => resolve11(true), timeoutMs);
13850
13850
  const done = (timedOut) => {
@@ -14581,7 +14581,7 @@ async function sessionMain(parsed, _args) {
14581
14581
  return;
14582
14582
  }
14583
14583
  }
14584
- var fs9, path12, VIEW_FILTERS, SECTION_LABELS, COMMAND_BLOCK, START_USAGE, STOP_USAGE, LIST_USAGE, VIEW_USAGE, HAR_USAGE, LOG_USAGE, productionStartWorld, startWorld;
14584
+ var fs9, path12, VIEW_FILTERS, SECTION_LABELS, SESSION_TAB_ATTACH_TIMEOUT_MS, COMMAND_BLOCK, START_USAGE, STOP_USAGE, LIST_USAGE, VIEW_USAGE, HAR_USAGE, LOG_USAGE, productionStartWorld, startWorld;
14585
14585
  var init_commands = __esm({
14586
14586
  "src/session/commands.ts"() {
14587
14587
  fs9 = __toESM(require("fs"));
@@ -14612,6 +14612,7 @@ var init_commands = __esm({
14612
14612
  recs: "motion",
14613
14613
  other: "other"
14614
14614
  };
14615
+ SESSION_TAB_ATTACH_TIMEOUT_MS = 5e3;
14615
14616
  COMMAND_BLOCK = `<command name="session">
14616
14617
  the artifact container \u2014 a session opens a tab, records HAR, and bundles every artifact; while active, every command auto-targets its tab
14617
14618
  use when starting scoped work against a page: start first, then every other capture command needs no --target/--port threading
@@ -14751,7 +14752,14 @@ effects:
14751
14752
  } catch (err) {
14752
14753
  throw worldFailure(`capture session start could not attach to ${url}: ${err instanceof Error ? err.message : err}. Reuse an existing tab with --target.`, err);
14753
14754
  }
14754
- return await waitForPageLoad(client, 1e4);
14755
+ try {
14756
+ return await waitForPageLoad(client, 1e4);
14757
+ } catch (err) {
14758
+ throw worldFailure(
14759
+ `capture session start could not attach to ${url}: Page.enable failed during its ${SESSION_TAB_ATTACH_TIMEOUT_MS}ms attachment window (${err instanceof Error ? err.message : err}). The CDP endpoint created the tab but cannot drive it; choose another endpoint from \`capture tab list\` and retry with \`--port\`.`,
14760
+ err
14761
+ );
14762
+ }
14755
14763
  } finally {
14756
14764
  client.close();
14757
14765
  }
@@ -15098,7 +15106,13 @@ async function resolveLiveTarget(client, input) {
15098
15106
  return resolveLiveCss(client, input, parsed.value);
15099
15107
  }
15100
15108
  }
15101
- function settleAxMatches(kind, input, matches) {
15109
+ function preferActionable(matches) {
15110
+ if (matches.length <= 1) return matches;
15111
+ const interactive = matches.filter((m) => m.role?.value !== void 0 && INTERACTIVE_ROLES.has(m.role.value));
15112
+ return interactive.length === 1 ? interactive : matches;
15113
+ }
15114
+ function settleAxMatches(kind, input, rawMatches) {
15115
+ const matches = preferActionable(rawMatches);
15102
15116
  if (matches.length === 1) {
15103
15117
  const m = matches[0];
15104
15118
  return {
@@ -241019,6 +241033,9 @@ init_motion();
241019
241033
  init_cdp();
241020
241034
  init_lib();
241021
241035
  var ROOTS = ["session", "page", "tab", "measure", "motion", "cdp", "lib"];
241036
+ var GUESSABLE_HINTS = {
241037
+ screenshot: "page shot"
241038
+ };
241022
241039
  var ROOT_BLOCKS = [
241023
241040
  COMMAND_BLOCK,
241024
241041
  COMMAND_BLOCK2,
@@ -241042,7 +241059,7 @@ CDP_PORT / CDP_TARGET pin the browser + tab for orchestrators \u2014 precedence:
241042
241059
  explicit flag > active session > env.`;
241043
241060
  }
241044
241061
  function printVersion() {
241045
- const declared = "1.3.12";
241062
+ const declared = "1.3.14";
241046
241063
  if (declared) {
241047
241064
  console.log(declared);
241048
241065
  return;
@@ -241072,10 +241089,11 @@ async function main() {
241072
241089
  if (ROOTS.includes(command) || command === "__bridge-serve") {
241073
241090
  return cdpMain();
241074
241091
  }
241092
+ const hint = GUESSABLE_HINTS[command];
241075
241093
  throw captureError(
241076
241094
  "invocation",
241077
241095
  "unknown_command",
241078
- `Unknown command ${command}; expected one of the seven roots: session, page, tab, measure, motion, cdp, lib.`
241096
+ hint ? `Unknown command ${command}; did you mean \`capture ${hint}\`? Expected one of the seven roots: session, page, tab, measure, motion, cdp, lib.` : `Unknown command ${command}; expected one of the seven roots: session, page, tab, measure, motion, cdp, lib.`
241079
241097
  );
241080
241098
  }
241081
241099
  main().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/capture",
3
- "version": "1.3.12",
3
+ "version": "1.3.14",
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"
package/src/capture.ts CHANGED
@@ -25,6 +25,18 @@ import { COMMAND_BLOCK as LIB_BLOCK } from './cdp/commands/lib.js';
25
25
  /** The seven visible root children, in help order. */
26
26
  const ROOTS = ['session', 'page', 'tab', 'measure', 'motion', 'cdp', 'lib'] as const;
27
27
 
28
+ /**
29
+ * Guessable former/legacy tokens with one unambiguous current destination —
30
+ * an orientation hint on the `unknown_command` error, never a second
31
+ * dispatchable path (the guessed token still fails; only the message
32
+ * changes). Deliberately small: an entry only earns a place here when its
33
+ * correct destination is unambiguous, so this stays a short discriminator
34
+ * rather than a growing synonym taxonomy.
35
+ */
36
+ const GUESSABLE_HINTS: Readonly<Record<string, string>> = {
37
+ screenshot: 'page shot',
38
+ };
39
+
28
40
  const ROOT_BLOCKS = [
29
41
  SESSION_BLOCK,
30
42
  PAGE_BLOCK,
@@ -90,10 +102,13 @@ async function main(): Promise<void> {
90
102
  return cdpMain();
91
103
  }
92
104
 
105
+ const hint = GUESSABLE_HINTS[command];
93
106
  throw captureError(
94
107
  'invocation',
95
108
  'unknown_command',
96
- `Unknown command ${command}; expected one of the seven roots: session, page, tab, measure, motion, cdp, lib.`,
109
+ hint
110
+ ? `Unknown command ${command}; did you mean \`capture ${hint}\`? Expected one of the seven roots: session, page, tab, measure, motion, cdp, lib.`
111
+ : `Unknown command ${command}; expected one of the seven roots: session, page, tab, measure, motion, cdp, lib.`,
97
112
  );
98
113
  }
99
114
 
package/src/interact.ts CHANGED
@@ -18,11 +18,19 @@
18
18
  * nothing — command leaves own all output; returned facts carry the resolved
19
19
  * identity (`backendNodeId`, role, name) and the dispatched coordinates.
20
20
  *
21
+ * `ax:`/`axid:` tie-break: a same-name match set that mixes an
22
+ * {@link INTERACTIVE_ROLES} role (e.g. `button`) with non-interactive ones
23
+ * (e.g. `StaticText` — the common shape of a label rendering inside its own
24
+ * button) narrows to the interactive match when that leaves exactly one
25
+ * candidate, because that is the only element the caller can actually drive.
26
+ * A tie among multiple interactive roles (or zero) stays genuinely
27
+ * ambiguous — see {@link preferActionable}.
28
+ *
21
29
  * Uses getFullAXTree instead of queryAXTree because queryAXTree hangs on
22
30
  * complex pages (7000+ AX nodes) when given the DOM root nodeId.
23
31
  */
24
32
 
25
- import { readFullAXTree, type FullAXNode } from './cdp/a11y.js';
33
+ import { INTERACTIVE_ROLES, readFullAXTree, type FullAXNode } from './cdp/a11y.js';
26
34
  import { isScrollDestination } from './cdp/leaf-grammar.js';
27
35
  import { captureError } from './errors.js';
28
36
  import { parseSelectorInput, type SelectorInputKind } from './output/selector.js';
@@ -163,11 +171,26 @@ export async function resolveLiveTarget(
163
171
  }
164
172
  }
165
173
 
174
+ /**
175
+ * Narrows a same-name match set to its lone {@link INTERACTIVE_ROLES} member
176
+ * when the set mixes interactive and non-interactive roles — e.g. a `button`
177
+ * plus the `StaticText` label rendered inside it, both carrying the same
178
+ * accessible name. Leaves `matches` untouched when zero or more than one
179
+ * interactive role is present, so a genuine tie (two buttons, or no
180
+ * interactive role at all) still surfaces every candidate.
181
+ */
182
+ function preferActionable(matches: FullAXNode[]): FullAXNode[] {
183
+ if (matches.length <= 1) return matches;
184
+ const interactive = matches.filter((m) => m.role?.value !== undefined && INTERACTIVE_ROLES.has(m.role.value));
185
+ return interactive.length === 1 ? interactive : matches;
186
+ }
187
+
166
188
  function settleAxMatches(
167
189
  kind: LiveTargetKind,
168
190
  input: string,
169
- matches: FullAXNode[],
191
+ rawMatches: FullAXNode[],
170
192
  ): ResolvedTarget | ResolutionFailure {
193
+ const matches = preferActionable(rawMatches);
171
194
  if (matches.length === 1) {
172
195
  const m = matches[0];
173
196
  return {
@@ -286,12 +286,18 @@ function generateId(): string {
286
286
  return `cap-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
287
287
  }
288
288
 
289
+ /** A fresh target must acknowledge Page.enable promptly. Target.createTarget
290
+ * already succeeded; waiting a full CDP request timeout here only postpones a
291
+ * failed session start before it can publish or roll back its target. */
292
+ export const SESSION_TAB_ATTACH_TIMEOUT_MS = 5_000;
293
+
289
294
  export async function waitForPageLoad(
290
- client: { waitReady(): Promise<void>; send(method: string, params?: Record<string, unknown>): Promise<unknown>; on(event: string, handler: (params: unknown) => void): void; },
295
+ client: { waitReady(): Promise<void>; send(method: string, params?: Record<string, unknown>, timeout?: number): Promise<unknown>; on(event: string, handler: (params: unknown) => void): void; },
291
296
  timeoutMs: number,
297
+ attachTimeoutMs = SESSION_TAB_ATTACH_TIMEOUT_MS,
292
298
  ): Promise<boolean> {
293
299
  await client.waitReady();
294
- await client.send('Page.enable');
300
+ await client.send('Page.enable', {}, attachTimeoutMs);
295
301
 
296
302
  return await new Promise<boolean>((resolve) => {
297
303
  const timer = setTimeout(() => resolve(true), timeoutMs);
@@ -502,7 +508,14 @@ const productionStartWorld: SessionStartWorld = {
502
508
  } catch (err) {
503
509
  throw worldFailure(`capture session start could not attach to ${url}: ${err instanceof Error ? err.message : err}. Reuse an existing tab with --target.`, err);
504
510
  }
505
- return await waitForPageLoad(client, 10_000);
511
+ try {
512
+ return await waitForPageLoad(client, 10_000);
513
+ } catch (err) {
514
+ throw worldFailure(
515
+ `capture session start could not attach to ${url}: Page.enable failed during its ${SESSION_TAB_ATTACH_TIMEOUT_MS}ms attachment window (${err instanceof Error ? err.message : err}). The CDP endpoint created the tab but cannot drive it; choose another endpoint from \`capture tab list\` and retry with \`--port\`.`,
516
+ err,
517
+ );
518
+ }
506
519
  } finally {
507
520
  client.close();
508
521
  }