@agentrhq/webcmd 0.3.0 → 0.3.2

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.
Files changed (183) hide show
  1. package/README.md +13 -2
  2. package/cli-manifest.json +228 -24
  3. package/clis/chatgpt/ask.js +1 -1
  4. package/clis/chatgpt/ask.test.js +11 -0
  5. package/clis/chatgpt/commands.test.js +102 -1
  6. package/clis/chatgpt/deep-research-result.js +45 -3
  7. package/clis/chatgpt/image.js +22 -2
  8. package/clis/chatgpt/project-file-add.js +24 -1
  9. package/clis/chatgpt/utils.js +258 -52
  10. package/clis/chatgpt/utils.test.js +259 -1
  11. package/clis/claude/ask.js +21 -1
  12. package/clis/facebook/feed.js +110 -24
  13. package/clis/facebook/feed.test.js +62 -0
  14. package/clis/facebook/search.js +185 -38
  15. package/clis/facebook/search.test.js +95 -50
  16. package/clis/instagram/explore.js +30 -12
  17. package/clis/instagram/explore.test.js +77 -0
  18. package/clis/instagram/post.js +14 -1
  19. package/clis/instagram/reel.js +13 -1
  20. package/clis/linkedin/company.js +153 -0
  21. package/clis/linkedin/company.test.js +111 -0
  22. package/clis/linkedin/connections.js +135 -0
  23. package/clis/linkedin/connections.test.js +141 -0
  24. package/clis/mercury/reimbursement-draft.js +12 -1
  25. package/clis/producthunt/hot.js +1 -2
  26. package/clis/spotify/spotify.js +11 -11
  27. package/clis/twitter/article-evaluate.test.js +27 -0
  28. package/clis/twitter/article.js +73 -16
  29. package/clis/twitter/article.test.js +209 -0
  30. package/clis/twitter/download.js +6 -1
  31. package/clis/twitter/post.js +14 -1
  32. package/clis/twitter/profile.js +1 -1
  33. package/clis/twitter/profile.test.js +8 -0
  34. package/clis/twitter/quote.js +11 -1
  35. package/clis/twitter/reply.js +11 -1
  36. package/dist/src/browser/base-page.d.ts +26 -0
  37. package/dist/src/browser/base-page.js +23 -8
  38. package/dist/src/browser/base-page.test.js +45 -12
  39. package/dist/src/browser/command-catalog.d.ts +6 -0
  40. package/dist/src/browser/command-catalog.js +259 -0
  41. package/dist/src/browser/command-catalog.test.d.ts +1 -0
  42. package/dist/src/browser/command-catalog.test.js +79 -0
  43. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
  44. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
  45. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
  46. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
  47. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +7 -0
  48. package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -9
  49. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
  50. package/dist/src/browser/target-resolver.d.ts +1 -0
  51. package/dist/src/browser/target-resolver.js +99 -4
  52. package/dist/src/browser/target-resolver.test.js +141 -1
  53. package/dist/src/build-manifest.d.ts +6 -0
  54. package/dist/src/build-manifest.js +18 -1
  55. package/dist/src/build-manifest.test.js +57 -1
  56. package/dist/src/builtin-command-surface.d.ts +9 -0
  57. package/dist/src/builtin-command-surface.js +16 -0
  58. package/dist/src/check-hosted-contract.test.d.ts +1 -0
  59. package/dist/src/check-hosted-contract.test.js +56 -0
  60. package/dist/src/cli.js +58 -106
  61. package/dist/src/cli.test.js +71 -1
  62. package/dist/src/command-presentation.d.ts +95 -0
  63. package/dist/src/command-presentation.js +486 -0
  64. package/dist/src/command-presentation.test.d.ts +1 -0
  65. package/dist/src/command-presentation.test.js +97 -0
  66. package/dist/src/command-surface.d.ts +43 -0
  67. package/dist/src/command-surface.js +205 -0
  68. package/dist/src/command-surface.test.d.ts +1 -0
  69. package/dist/src/command-surface.test.js +406 -0
  70. package/dist/src/commanderAdapter.d.ts +5 -1
  71. package/dist/src/commanderAdapter.js +12 -55
  72. package/dist/src/commanderAdapter.test.js +15 -3
  73. package/dist/src/community-plugin-sync.d.ts +11 -0
  74. package/dist/src/community-plugin-sync.js +138 -0
  75. package/dist/src/community-plugin-sync.test.d.ts +1 -0
  76. package/dist/src/community-plugin-sync.test.js +123 -0
  77. package/dist/src/completion-fast.d.ts +4 -4
  78. package/dist/src/completion-fast.js +15 -34
  79. package/dist/src/completion-shared.d.ts +4 -0
  80. package/dist/src/completion-shared.js +38 -0
  81. package/dist/src/completion.js +3 -27
  82. package/dist/src/completion.test.js +31 -2
  83. package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
  84. package/dist/src/docs-sync-review-cli.test.js +322 -0
  85. package/dist/src/docs-sync-review.d.ts +117 -0
  86. package/dist/src/docs-sync-review.js +475 -0
  87. package/dist/src/docs-sync-review.test.d.ts +1 -0
  88. package/dist/src/docs-sync-review.test.js +324 -0
  89. package/dist/src/errors.d.ts +12 -3
  90. package/dist/src/errors.js +11 -7
  91. package/dist/src/errors.test.js +14 -1
  92. package/dist/src/execution.d.ts +1 -2
  93. package/dist/src/execution.js +4 -45
  94. package/dist/src/generate-release-notes-cli.test.js +7 -2
  95. package/dist/src/help.d.ts +4 -0
  96. package/dist/src/help.js +50 -255
  97. package/dist/src/help.test.js +27 -1
  98. package/dist/src/hosted/args.d.ts +2 -7
  99. package/dist/src/hosted/args.js +2 -99
  100. package/dist/src/hosted/args.test.js +15 -1
  101. package/dist/src/hosted/availability.d.ts +13 -0
  102. package/dist/src/hosted/availability.js +16 -0
  103. package/dist/src/hosted/availability.test.d.ts +1 -0
  104. package/dist/src/hosted/availability.test.js +191 -0
  105. package/dist/src/hosted/browser-args.d.ts +18 -0
  106. package/dist/src/hosted/browser-args.js +152 -0
  107. package/dist/src/hosted/browser-args.test.d.ts +1 -0
  108. package/dist/src/hosted/browser-args.test.js +182 -0
  109. package/dist/src/hosted/client.d.ts +31 -2
  110. package/dist/src/hosted/client.js +441 -53
  111. package/dist/src/hosted/client.test.js +651 -32
  112. package/dist/src/hosted/config.d.ts +7 -2
  113. package/dist/src/hosted/config.js +26 -4
  114. package/dist/src/hosted/config.test.js +34 -1
  115. package/dist/src/hosted/contract.d.ts +94 -0
  116. package/dist/src/hosted/contract.js +208 -0
  117. package/dist/src/hosted/contract.test.d.ts +1 -0
  118. package/dist/src/hosted/contract.test.js +361 -0
  119. package/dist/src/hosted/credentials.d.ts +38 -0
  120. package/dist/src/hosted/credentials.js +248 -0
  121. package/dist/src/hosted/credentials.test.d.ts +1 -0
  122. package/dist/src/hosted/credentials.test.js +93 -0
  123. package/dist/src/hosted/file-contract.test.d.ts +1 -0
  124. package/dist/src/hosted/file-contract.test.js +174 -0
  125. package/dist/src/hosted/files.d.ts +29 -0
  126. package/dist/src/hosted/files.js +296 -0
  127. package/dist/src/hosted/files.test.d.ts +1 -0
  128. package/dist/src/hosted/files.test.js +231 -0
  129. package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
  130. package/dist/src/hosted/main-lifecycle.test.js +213 -0
  131. package/dist/src/hosted/manifest.d.ts +5 -0
  132. package/dist/src/hosted/manifest.js +21 -66
  133. package/dist/src/hosted/manifest.test.js +120 -2
  134. package/dist/src/hosted/output-parity.test.d.ts +1 -0
  135. package/dist/src/hosted/output-parity.test.js +108 -0
  136. package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
  137. package/dist/src/hosted/root-command-surface.test.js +646 -0
  138. package/dist/src/hosted/runner.d.ts +7 -0
  139. package/dist/src/hosted/runner.js +554 -253
  140. package/dist/src/hosted/runner.test.js +1471 -53
  141. package/dist/src/hosted/setup.d.ts +3 -2
  142. package/dist/src/hosted/setup.js +45 -15
  143. package/dist/src/hosted/setup.test.js +133 -6
  144. package/dist/src/hosted/types.d.ts +101 -23
  145. package/dist/src/main.js +120 -108
  146. package/dist/src/manifest-types.d.ts +2 -0
  147. package/dist/src/output.d.ts +17 -2
  148. package/dist/src/output.js +88 -81
  149. package/dist/src/output.test.js +141 -51
  150. package/dist/src/pipeline/executor.test.js +1 -0
  151. package/dist/src/pipeline/steps/download.test.js +1 -0
  152. package/dist/src/plugin-create-cli.test.d.ts +1 -0
  153. package/dist/src/plugin-create-cli.test.js +37 -0
  154. package/dist/src/plugin-manifest.d.ts +12 -0
  155. package/dist/src/plugin-manifest.js +16 -0
  156. package/dist/src/plugin-manifest.test.js +19 -1
  157. package/dist/src/plugin-scaffold.d.ts +4 -1
  158. package/dist/src/plugin-scaffold.js +4 -1
  159. package/dist/src/plugin-scaffold.test.js +23 -8
  160. package/dist/src/plugin.js +4 -1
  161. package/dist/src/plugin.test.js +13 -0
  162. package/dist/src/registry.d.ts +2 -0
  163. package/dist/src/release-notes.js +12 -4
  164. package/dist/src/release-notes.test.js +27 -15
  165. package/dist/src/root-command-surface.d.ts +31 -0
  166. package/dist/src/root-command-surface.js +106 -0
  167. package/dist/src/serialization.d.ts +1 -16
  168. package/dist/src/serialization.js +5 -55
  169. package/dist/src/stream-write.d.ts +12 -0
  170. package/dist/src/stream-write.js +91 -0
  171. package/dist/src/stream-write.test.d.ts +1 -0
  172. package/dist/src/stream-write.test.js +186 -0
  173. package/dist/src/types.d.ts +5 -0
  174. package/dist/src/utils.d.ts +1 -1
  175. package/dist/src/utils.js +2 -8
  176. package/dist/src/utils.test.js +50 -0
  177. package/hosted-contract.json +36759 -0
  178. package/package.json +6 -1
  179. package/scripts/check-hosted-contract.mjs +108 -0
  180. package/scripts/docs-sync-review.ts +332 -0
  181. package/scripts/sync-community-plugins.ts +14 -0
  182. package/skills/webcmd-autofix/SKILL.md +26 -37
  183. package/skills/webcmd-usage/SKILL.md +7 -0
@@ -243,7 +243,7 @@ describe('BasePage native input routing', () => {
243
243
  .mockResolvedValueOnce({ root: { nodeId: 1 } })
244
244
  .mockResolvedValueOnce({ nodeId: 9 })
245
245
  .mockResolvedValueOnce({});
246
- page.results = [resolveOk, { x: 50, y: 100, w: 200, h: 32, visible: true }];
246
+ page.results = [resolveOk, { x: 50, y: 100, w: 200, h: 32, visible: true, hit: 'target' }];
247
247
  page.withArgsResults = [{ ok: true, multiple: false, accept: 'application/pdf' }, undefined];
248
248
  await page.click('#save');
249
249
  expect(page.cdp).toHaveBeenCalledWith('DOM.scrollIntoViewIfNeeded', { nodeId: 9 });
@@ -253,14 +253,38 @@ describe('BasePage native input routing', () => {
253
253
  it('clicks via CDP Input.dispatchMouseEvent when rect is visible', async () => {
254
254
  const page = new ActionPage();
255
255
  page.nativeClick = vi.fn().mockResolvedValue(undefined);
256
- page.results = [resolveOk, { x: 50, y: 100, w: 200, h: 32, visible: true }];
257
- await page.click('#category');
256
+ page.results = [resolveOk, { x: 50, y: 100, w: 200, h: 32, visible: true, hit: 'target', retargeted: false }];
257
+ await expect(page.click('#category')).resolves.toEqual({
258
+ matches_n: 1,
259
+ match_level: 'exact',
260
+ click_method: 'cdp',
261
+ hit: 'target',
262
+ retargeted: false,
263
+ });
258
264
  expect(page.nativeClick).toHaveBeenCalledWith(50, 100);
259
265
  expect(page.nativeClick).toHaveBeenCalledTimes(1);
260
266
  expect(page.scripts).toHaveLength(2);
261
267
  expect(page.scripts[1]).toContain('getBoundingClientRect');
262
268
  expect(page.scripts.join('\n')).not.toContain('el.click()');
263
269
  });
270
+ it('falls back to direct JS click when an unrelated overlay covers the target', async () => {
271
+ const page = new ActionPage();
272
+ page.nativeClick = vi.fn().mockResolvedValue(undefined);
273
+ page.results = [
274
+ resolveOk,
275
+ { x: 50, y: 100, w: 200, h: 32, visible: true, hit: 'other', retargeted: false },
276
+ { status: 'clicked', x: 50, y: 100 },
277
+ ];
278
+ await expect(page.click('#covered')).resolves.toEqual({
279
+ matches_n: 1,
280
+ match_level: 'exact',
281
+ click_method: 'js',
282
+ hit: 'other',
283
+ retargeted: false,
284
+ });
285
+ expect(page.nativeClick).not.toHaveBeenCalled();
286
+ expect(page.scripts.at(-1)).toContain('el.click()');
287
+ });
264
288
  it('clicks AX snapshot refs through backend node coordinates without DOM resolver', async () => {
265
289
  const page = new ActionPage();
266
290
  page.nativeClick = vi.fn().mockResolvedValue(undefined);
@@ -284,7 +308,7 @@ describe('BasePage native input routing', () => {
284
308
  return {};
285
309
  });
286
310
  await expect(page.snapshot({ source: 'ax' })).resolves.toContain('[1]button "Submit"');
287
- await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'exact' });
311
+ await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'exact', click_method: 'ax' });
288
312
  expect(page.cdp).toHaveBeenNthCalledWith(1, 'Accessibility.enable', {});
289
313
  expect(page.cdp).toHaveBeenNthCalledWith(2, 'Accessibility.getFullAXTree', {});
290
314
  expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', {});
@@ -343,7 +367,7 @@ describe('BasePage native input routing', () => {
343
367
  expect(snapshot).toContain('[2]button "Frame Save"');
344
368
  expect(snapshot).toContain('frame "https://other.example/embed":');
345
369
  expect(snapshot).toContain('[3]button "Cross Save"');
346
- await expect(page.click('2')).resolves.toEqual({ matches_n: 1, match_level: 'exact' });
370
+ await expect(page.click('2')).resolves.toEqual({ matches_n: 1, match_level: 'exact', click_method: 'ax' });
347
371
  expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', { frameId: 'same-frame' });
348
372
  expect(page.cdp).toHaveBeenCalledWith('Accessibility.enable', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
349
373
  expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
@@ -380,7 +404,7 @@ describe('BasePage native input routing', () => {
380
404
  });
381
405
  const snapshot = await page.snapshot({ source: 'ax' });
382
406
  expect(snapshot).toContain('[1]button "Cross Save"');
383
- await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'exact' });
407
+ await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'exact', click_method: 'ax' });
384
408
  expect(page.cdp).toHaveBeenCalledWith('Accessibility.enable', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
385
409
  expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
386
410
  expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 99, frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
@@ -420,7 +444,7 @@ describe('BasePage native input routing', () => {
420
444
  return {};
421
445
  });
422
446
  await page.snapshot({ source: 'ax' });
423
- await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified' });
447
+ await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified', click_method: 'ax' });
424
448
  expect(page.cdp).toHaveBeenCalledWith('Accessibility.enable', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
425
449
  expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', { frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
426
450
  expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 99, frameId: 'cross-frame', sessionId: 'target', targetUrl: 'https://other.example/embed' });
@@ -465,7 +489,7 @@ describe('BasePage native input routing', () => {
465
489
  return {};
466
490
  });
467
491
  await page.snapshot({ source: 'ax' });
468
- await expect(page.click('2')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified' });
492
+ await expect(page.click('2')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified', click_method: 'ax' });
469
493
  expect(page.cdp).toHaveBeenCalledWith('Accessibility.enable', {});
470
494
  expect(page.cdp).toHaveBeenCalledWith('Accessibility.getFullAXTree', { frameId: 'same-frame' });
471
495
  expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 20 });
@@ -494,7 +518,7 @@ describe('BasePage native input routing', () => {
494
518
  return {};
495
519
  });
496
520
  await page.snapshot({ source: 'ax' });
497
- await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified' });
521
+ await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified', click_method: 'ax' });
498
522
  expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 10 });
499
523
  expect(page.cdp).toHaveBeenCalledWith('DOM.getBoxModel', { backendNodeId: 42 });
500
524
  expect(page.nativeClick).toHaveBeenCalledWith(120, 210);
@@ -525,7 +549,7 @@ describe('BasePage native input routing', () => {
525
549
  for (let i = 0; i < 10; i++) {
526
550
  staleBackendIds.add(currentBackendId);
527
551
  currentBackendId += 1;
528
- await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified' });
552
+ await expect(page.click('1')).resolves.toEqual({ matches_n: 1, match_level: 'reidentified', click_method: 'ax' });
529
553
  }
530
554
  expect(page.nativeClick).toHaveBeenCalledTimes(10);
531
555
  });
@@ -562,7 +586,7 @@ describe('BasePage native input routing', () => {
562
586
  page.nativeClick = nativeClick;
563
587
  page.results = [
564
588
  resolveOk,
565
- { x: 10, y: 20, w: 100, h: 30, visible: true },
589
+ { x: 10, y: 20, w: 100, h: 30, visible: true, hit: 'target' },
566
590
  { status: 'js_failed', x: 10, y: 20, error: 'click intercepted' },
567
591
  ];
568
592
  await page.click('#flaky');
@@ -624,7 +648,7 @@ describe('BasePage native input routing', () => {
624
648
  resolveOk,
625
649
  { ok: true, checked: false, disabled: false, kind: 'checkbox' },
626
650
  resolveOk,
627
- { x: 20, y: 30, w: 40, h: 20, visible: true },
651
+ { x: 20, y: 30, w: 40, h: 20, visible: true, hit: 'target' },
628
652
  { ok: true, checked: true, disabled: false, kind: 'checkbox' },
629
653
  ];
630
654
  await expect(page.setChecked('#agree', true)).resolves.toEqual({
@@ -801,3 +825,12 @@ describe('BasePage.evaluateWithArgs scoping', () => {
801
825
  `, { value: 20 })).resolves.toBe(42);
802
826
  });
803
827
  });
828
+ describe('BasePage.sleep', () => {
829
+ it('is a pure client-side sleep that never evaluates page script', async () => {
830
+ const page = new ActionPage();
831
+ const evaluateSpy = vi.spyOn(page, 'evaluate');
832
+ await page.sleep(0);
833
+ expect(evaluateSpy).not.toHaveBeenCalled();
834
+ expect(page.scripts).toHaveLength(0);
835
+ });
836
+ });
@@ -0,0 +1,6 @@
1
+ import type { HostedArgumentContract, HostedBrowserCommandContract } from '../hosted/contract.js';
2
+ /** Exact local Commander flags for every catalogued browser option. */
3
+ export declare function browserOptionFlags(option: HostedArgumentContract): string;
4
+ /** Shared Commander value parser for browser options with structural coercion. */
5
+ export declare function browserOptionValueParser(commandPath: string, optionName: string): ((value: string) => unknown) | undefined;
6
+ export declare const browserCommandCatalog: readonly HostedBrowserCommandContract[];
@@ -0,0 +1,259 @@
1
+ import { InvalidArgumentError } from 'commander';
2
+ function argument(name, type, description, positional, metadata = {}) {
3
+ return {
4
+ name,
5
+ type,
6
+ description,
7
+ positional,
8
+ required: metadata.required === true,
9
+ variadic: metadata.variadic === true,
10
+ ...(metadata.default !== undefined ? { default: metadata.default } : {}),
11
+ ...(metadata.choices?.length ? { choices: [...metadata.choices] } : {}),
12
+ };
13
+ }
14
+ function positional(name, description = '', metadata = {}) {
15
+ return argument(name, 'string', description, true, metadata);
16
+ }
17
+ function option(name, description, metadata = {}) {
18
+ return argument(name, 'string', description, false, metadata);
19
+ }
20
+ function flag(name, description, defaultValue) {
21
+ return argument(name, 'boolean', description, false, {
22
+ ...(defaultValue !== undefined ? { default: defaultValue } : {}),
23
+ });
24
+ }
25
+ const BROWSER_OPTION_VALUE_NAMES = {
26
+ amount: 'pixels',
27
+ as: 'format',
28
+ childrenMax: 'n',
29
+ chunkSize: 'chars',
30
+ css: 'selector',
31
+ depth: 'n',
32
+ detail: 'key',
33
+ filter: 'fields',
34
+ frame: 'index',
35
+ fromLabel: 'text',
36
+ fromName: 'text',
37
+ fromNth: 'n',
38
+ fromRole: 'role',
39
+ fromTestid: 'id',
40
+ fromText: 'text',
41
+ height: 'n',
42
+ index: 'n',
43
+ label: 'text',
44
+ level: 'level',
45
+ limit: 'n',
46
+ max: 'n',
47
+ maxBody: 'chars',
48
+ name: 'text',
49
+ nth: 'n',
50
+ page: 'id',
51
+ role: 'role',
52
+ seedArgs: 'value',
53
+ selector: 'css',
54
+ since: 'duration',
55
+ source: 'source',
56
+ start: 'char',
57
+ tab: 'targetId',
58
+ testid: 'id',
59
+ text: 'text',
60
+ textMax: 'n',
61
+ timeout: 'ms',
62
+ toLabel: 'text',
63
+ toName: 'text',
64
+ toNth: 'n',
65
+ toRole: 'role',
66
+ toTestid: 'id',
67
+ toText: 'text',
68
+ trace: 'mode',
69
+ ttl: 'ms',
70
+ until: 'duration',
71
+ width: 'n',
72
+ };
73
+ /** Exact local Commander flags for every catalogued browser option. */
74
+ export function browserOptionFlags(option) {
75
+ const longName = option.name.replace(/[A-Z]/g, character => `-${character.toLowerCase()}`);
76
+ if (option.type === 'boolean')
77
+ return option.name === 'fixture' ? '--no-fixture' : `--${longName}`;
78
+ const valueName = BROWSER_OPTION_VALUE_NAMES[option.name];
79
+ if (!valueName)
80
+ throw new Error(`Browser option --${longName} is missing its Commander value name`);
81
+ return `--${longName} <${valueName}>`;
82
+ }
83
+ /** Shared Commander value parser for browser options with structural coercion. */
84
+ export function browserOptionValueParser(commandPath, optionName) {
85
+ if (commandPath !== 'screenshot' || (optionName !== 'width' && optionName !== 'height'))
86
+ return undefined;
87
+ return (value) => {
88
+ if (!/^\d+$/.test(value) || Number.parseInt(value, 10) <= 0) {
89
+ throw new InvalidArgumentError(`--${optionName} must be a positive integer (got "${value}")`);
90
+ }
91
+ return Number.parseInt(value, 10);
92
+ };
93
+ }
94
+ function command(commandPath, description, action, positionals = [], options = [], sessionPolicy = 'require-existing') {
95
+ return {
96
+ command: commandPath,
97
+ aliases: [],
98
+ description,
99
+ positionals,
100
+ options,
101
+ sessionPolicy,
102
+ ...(action !== undefined ? { action } : {}),
103
+ };
104
+ }
105
+ const TAB_TARGET_HELP = 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"';
106
+ const TARGET_HELP = 'Numeric ref (from browser state / find), CSS selector, or omit when using --role/--name/etc.';
107
+ const WRITE_NTH_HELP = 'When <target> is a multi-match CSS selector, pick the nth match (0-based)';
108
+ const READ_NTH_HELP = 'Pick the nth match (0-based) when <target> is a multi-match CSS selector';
109
+ const tabOption = option('tab', TAB_TARGET_HELP);
110
+ const semanticLocatorOptions = [
111
+ option('role', 'Semantic role (button, link, textbox, option, etc.)'),
112
+ option('name', 'Accessible name contains text (aria-label, label, title, placeholder, or visible text)'),
113
+ option('label', 'Associated label contains text'),
114
+ option('text', 'Visible text contains text'),
115
+ option('testid', 'data-testid / data-test / test-id contains id'),
116
+ ];
117
+ const semanticWriteOptions = [
118
+ ...semanticLocatorOptions,
119
+ option('nth', WRITE_NTH_HELP),
120
+ tabOption,
121
+ ];
122
+ const semanticReadOptions = [
123
+ ...semanticLocatorOptions,
124
+ option('nth', READ_NTH_HELP),
125
+ tabOption,
126
+ ];
127
+ export const browserCommandCatalog = [
128
+ command('analyze', 'Classify site: anti-bot vendor, real-data API candidates, pattern (A/B/C/D), nearest adapter, next step', 'analyze', [positional('url', '', { required: true })], [tabOption], 'create-or-reuse'),
129
+ command('back', 'Go back in browser history', 'back', [], [tabOption]),
130
+ command('bind', 'Bind an existing Cloak runtime tab to the browser session named by <session>', undefined, [], [
131
+ option('page', 'Cloak tab page id from `webcmd browser <session> tab list`'),
132
+ option('index', 'Cloak tab index from `webcmd browser <session> tab list`'),
133
+ ], 'local-only'),
134
+ command('check', 'Ensure a checkbox/radio/aria-checked control is checked — JSON envelope {checked, changed, target, matches_n}', 'check', [positional('target', TARGET_HELP)], semanticWriteOptions),
135
+ command('click', 'Click element — JSON envelope {clicked, target, matches_n}', 'click', [positional('target', TARGET_HELP)], semanticWriteOptions),
136
+ command('close', 'Release the current browser session tab lease', 'close-window', [], [], 'close-existing'),
137
+ command('console', 'Read recent browser console messages', 'console', [], [
138
+ tabOption,
139
+ option('level', 'Console level: all, error, warning, log, info, debug', { default: 'all' }),
140
+ option('since', 'Only include messages from the last duration (for example: 30s, 2m)'),
141
+ option('until', 'Only include messages older than the duration from now'),
142
+ flag('follow', 'Continuously print new console messages as JSON lines', false),
143
+ ]),
144
+ command('dblclick', 'Double-click element — JSON envelope {dblclicked, target, matches_n}', 'dblclick', [positional('target', TARGET_HELP)], semanticWriteOptions),
145
+ command('dialog/accept', 'Accept the currently open JavaScript dialog', 'dialog-accept', [], [option('text', 'Prompt text to submit for prompt() dialogs'), tabOption]),
146
+ command('dialog/dismiss', 'Dismiss the currently open JavaScript dialog', 'dialog-dismiss', [], [tabOption]),
147
+ command('drag', 'Drag one element to another — JSON envelope {dragged, source, target, source_matches_n, target_matches_n}', 'drag', [
148
+ positional('source', 'Numeric ref/CSS selector to drag from, or omit with --from-role/--from-name/etc.'),
149
+ positional('target', 'Numeric ref/CSS selector to drop onto, or omit with --to-role/--to-name/etc.'),
150
+ ], [
151
+ option('fromRole', 'from semantic role'),
152
+ option('fromName', 'from accessible name contains text'),
153
+ option('fromLabel', 'from associated label contains text'),
154
+ option('fromText', 'from visible text contains text'),
155
+ option('fromTestid', 'from data-testid / data-test / test-id contains id'),
156
+ option('toRole', 'to semantic role'),
157
+ option('toName', 'to accessible name contains text'),
158
+ option('toLabel', 'to associated label contains text'),
159
+ option('toText', 'to visible text contains text'),
160
+ option('toTestid', 'to data-testid / data-test / test-id contains id'),
161
+ option('fromNth', 'When <source> is a multi-match CSS selector, pick the nth match (0-based)'),
162
+ option('toNth', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)'),
163
+ tabOption,
164
+ ]),
165
+ command('eval', 'Execute JS in page context, return result', 'exec', [positional('js', 'JavaScript code', { required: true })], [option('frame', 'Cross-origin iframe index from "browser frames"'), tabOption]),
166
+ command('extract', 'Extract page content as markdown, paragraph-aware chunks for long pages', 'extract', [], [
167
+ option('selector', 'CSS selector scope; defaults to <main>/<article>/<body>'),
168
+ option('chunkSize', 'Target chunk size in chars', { default: '20000' }),
169
+ option('start', 'Start offset (use next_start_char from a previous extract)', { default: '0' }),
170
+ tabOption,
171
+ ]),
172
+ command('fill', 'Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}', 'fill', [
173
+ positional('targetOrText', 'Numeric ref/CSS target, or text when using --role/--name/etc.'),
174
+ positional('text', 'Text to set exactly'),
175
+ ], semanticWriteOptions),
176
+ command('find', 'Find DOM elements by CSS or semantic locator — returns JSON {matches_n, entries[]}', 'find', [], [
177
+ ...semanticLocatorOptions,
178
+ option('css', 'CSS selector (required)'),
179
+ option('limit', 'Max entries returned', { default: '50' }),
180
+ option('textMax', 'Max chars of trimmed text per entry', { default: '120' }),
181
+ tabOption,
182
+ ]),
183
+ command('focus', 'Focus an element — JSON envelope {focused, target, matches_n}', 'focus', [positional('target', TARGET_HELP)], semanticWriteOptions),
184
+ command('frames', 'List cross-origin iframe targets in snapshot order', 'frames', [], [tabOption]),
185
+ command('get/attributes', 'Element attributes — JSON envelope {value, matches_n}', 'get-attributes', [positional('target', TARGET_HELP)], semanticReadOptions),
186
+ command('get/html', 'Page HTML (or scoped); use --as json for a {tag, attrs, text, children} tree', 'get-html', [], [
187
+ option('selector', 'CSS selector scope (first match)'),
188
+ option('as', 'Output format: "html" (default) or "json" for structured tree', { default: 'html' }),
189
+ option('max', 'Max characters of raw HTML to return (0 = unlimited)', { default: '0' }),
190
+ option('depth', '(--as json) Max tree depth below root (0 = root only, 0 disables = unlimited via empty)', { default: '' }),
191
+ option('childrenMax', '(--as json) Max element children kept per node (empty = unlimited)', { default: '' }),
192
+ option('textMax', '(--as json) Max chars of direct text kept per node (empty = unlimited)', { default: '' }),
193
+ tabOption,
194
+ ]),
195
+ command('get/text', 'Element text content — JSON envelope {value, matches_n}', 'get-text', [positional('target', TARGET_HELP)], semanticReadOptions),
196
+ command('get/title', 'Page title', 'get-title', [], [tabOption]),
197
+ command('get/url', 'Current page URL', 'get-url', [], [tabOption]),
198
+ command('get/value', 'Input/textarea value — JSON envelope {value, matches_n}', 'get-value', [positional('target', TARGET_HELP)], semanticReadOptions),
199
+ command('hover', 'Move the mouse over an element — JSON envelope {hovered, target, matches_n}', 'hover', [positional('target', TARGET_HELP)], semanticWriteOptions),
200
+ command('init', 'Generate adapter scaffold in ~/.webcmd/clis/', 'init', [positional('name', 'Adapter name in site/command format (e.g. hn/top)', { required: true })]),
201
+ command('keys', 'Press keyboard key', 'press-key', [positional('key', 'Key to press (Enter, Escape, Tab, Control+a)', { required: true })], [tabOption]),
202
+ command('network', 'Capture network requests as shape previews; retrieve full bodies by key', 'network', [], [
203
+ tabOption,
204
+ option('detail', 'Emit full body for the entry with this key'),
205
+ flag('all', 'Include static resources (js/css/images/telemetry)'),
206
+ flag('raw', 'Emit full bodies for every entry (skip shape preview)'),
207
+ option('filter', 'Comma-separated field names; keep only entries whose body shape has ALL names as path segments'),
208
+ option('since', 'Only include entries from the last duration (for example: 30s, 2m)'),
209
+ option('until', 'Only include entries older than the duration from now'),
210
+ flag('follow', 'Continuously print new matching entries as JSON lines', false),
211
+ flag('failed', 'Only include failed HTTP requests (status 0 or >= 400)', false),
212
+ option('maxBody', 'With --detail: cap the emitted body at N chars (0 = unlimited, default)', { default: '0' }),
213
+ option('ttl', 'Cache TTL in ms for --detail lookups', { default: '86400000' }),
214
+ ]),
215
+ command('open', 'Open URL in the browser session', 'navigate', [positional('url', '', { required: true })], [tabOption], 'create-or-reuse'),
216
+ command('screenshot', 'Take screenshot', 'screenshot', [positional('path', 'Save to file (base64 if omitted)')], [
217
+ tabOption,
218
+ flag('fullPage', 'Capture the full scrollable page, not just the viewport', false),
219
+ flag('annotate', 'Overlay visible browser state ref labels on the screenshot', false),
220
+ option('width', 'Override viewport width in CSS pixels for this screenshot only'),
221
+ option('height', 'Override viewport height in CSS pixels for this screenshot only (ignored with --full-page)'),
222
+ ]),
223
+ command('scroll', 'Scroll page', 'scroll', [positional('direction', 'up or down', { required: true })], [option('amount', 'Pixels to scroll', { default: '500' }), tabOption]),
224
+ command('select', 'Select dropdown option — JSON envelope {selected, target, matches_n}', 'select', [
225
+ positional('targetOrOption', 'Numeric ref/CSS target, or option text when using --role/--name/etc.'),
226
+ positional('option', 'Option text (or value) to select'),
227
+ ], semanticWriteOptions),
228
+ command('state', 'Page state: URL, title, interactive elements with [N] indices', 'snapshot', [], [
229
+ option('source', 'Snapshot backend: dom (default) or ax prototype', { default: 'dom' }),
230
+ flag('compareSources', 'Print DOM vs AX snapshot metrics for observation promotion decisions', false),
231
+ tabOption,
232
+ ]),
233
+ command('tab/close', 'Close a tab by target ID', 'tabs', [positional('targetId', TAB_TARGET_HELP)], [tabOption]),
234
+ command('tab/list', 'List tabs in the browser session with target IDs', 'tabs'),
235
+ command('tab/new', 'Create a new tab and print its target ID', 'tabs', [positional('url', 'Optional URL to open in the new tab')]),
236
+ command('tab/select', 'Select a tab by target ID and make it the default browser tab', 'tabs', [positional('targetId', TAB_TARGET_HELP)], [tabOption]),
237
+ command('type', 'Click element, then type text — JSON envelope {typed, text, target, matches_n, autocomplete}', 'type', [
238
+ positional('targetOrText', 'Numeric ref/CSS target, or text when using --role/--name/etc.'),
239
+ positional('text', 'Text to type'),
240
+ ], semanticWriteOptions),
241
+ command('unbind', 'Compatibility command; release the Cloak browser session named by <session>', 'close-window', [], [], 'close-existing'),
242
+ command('uncheck', 'Ensure a checkbox/aria-checked control is unchecked — JSON envelope {checked, changed, target, matches_n}', 'uncheck', [positional('target', TARGET_HELP)], semanticWriteOptions),
243
+ command('upload', 'Attach local files to a file input — JSON envelope {uploaded, files, file_names, target, matches_n}', 'set-file-input', [
244
+ positional('targetOrFile', 'Numeric ref/CSS target, or first file when using --role/--name/etc.'),
245
+ positional('files', 'Local file path(s) to attach', { variadic: true }),
246
+ ], semanticWriteOptions),
247
+ command('verify', 'Execute an adapter and validate output; uses fixture at ~/.webcmd/sites/<site>/verify/<cmd>.json when present', 'verify', [positional('name', 'Adapter name in site/command format (e.g. hn/top)', { required: true })], [
248
+ flag('writeFixture', 'Write a starter fixture to ~/.webcmd/sites/<site>/verify/<command>.json if none exists'),
249
+ flag('updateFixture', 'Overwrite an existing fixture with one derived from current output'),
250
+ flag('fixture', 'Ignore any fixture file for this run (no value-level validation)'),
251
+ flag('strictMemory', 'Fail (not just warn) when ~/.webcmd/sites/<site>/endpoints.json or notes.md is missing'),
252
+ option('seedArgs', 'Seed args when no fixture exists; use JSON array/object for multiple args or flags'),
253
+ option('trace', 'Trace capture for the adapter subprocess: off, on, retain-on-failure', { default: 'off' }),
254
+ ]),
255
+ command('wait', 'Wait for selector, text, time, matching XHR, or browser download (e.g. wait selector ".loaded", wait text "Success", wait time 3, wait xhr "/api/search", wait download receipt.pdf)', 'wait', [
256
+ positional('type', 'selector, text, time, xhr, or download', { required: true }),
257
+ positional('value', 'CSS selector, text string, seconds, XHR URL regex, or download filename/URL pattern'),
258
+ ], [tabOption, option('timeout', 'Timeout in milliseconds', { default: '10000' })]),
259
+ ];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createProgram } from '../cli.js';
3
+ import { commanderNamespaceHelpData } from '../help.js';
4
+ import { buildHostedContract, } from '../hosted/contract.js';
5
+ import { browserCommandCatalog, browserOptionFlags } from './command-catalog.js';
6
+ function browserCommand() {
7
+ const browser = createProgram('', '').commands.find(command => command.name() === 'browser');
8
+ if (!browser)
9
+ throw new Error('Local browser command is not registered');
10
+ return browser;
11
+ }
12
+ function localBrowserCommands() {
13
+ const help = commanderNamespaceHelpData(browserCommand());
14
+ return help.commands;
15
+ }
16
+ function normalizeArgument(argument, positional) {
17
+ return {
18
+ name: argument.name,
19
+ type: positional || argument.takes_value ? 'string' : 'boolean',
20
+ description: argument.help ?? '',
21
+ positional,
22
+ required: positional && argument.required === true,
23
+ variadic: argument.variadic === true,
24
+ ...(argument.default !== undefined ? { default: argument.default } : {}),
25
+ ...(argument.choices?.length ? { choices: [...argument.choices] } : {}),
26
+ };
27
+ }
28
+ function normalizeLocalCommand(command) {
29
+ return {
30
+ command: command.name.replaceAll(' ', '/'),
31
+ aliases: [...(command.aliases ?? [])],
32
+ description: command.description,
33
+ positionals: command.positionals.map(argument => normalizeArgument(argument, true)),
34
+ options: command.command_options.map(argument => normalizeArgument(argument, false)),
35
+ };
36
+ }
37
+ describe('browserCommandCatalog', () => {
38
+ it('catalogues the exact local Commander browser leaf set and metadata', () => {
39
+ const local = localBrowserCommands().map(normalizeLocalCommand);
40
+ const catalogPaths = new Set(browserCommandCatalog.map(command => command.command));
41
+ const localPaths = new Set(local.map(command => command.command));
42
+ expect({
43
+ uncatalogued: [...localPaths].filter(command => !catalogPaths.has(command)).sort(),
44
+ stale: [...catalogPaths].filter(command => !localPaths.has(command)).sort(),
45
+ }).toEqual({ uncatalogued: [], stale: [] });
46
+ expect(browserCommandCatalog.map(({ sessionPolicy: _sessionPolicy, action: _action, ...command }) => command))
47
+ .toEqual(local);
48
+ });
49
+ it('preserves the exact local Commander flag grammar for every browser option', () => {
50
+ const local = new Map(localBrowserCommands().map(command => [
51
+ command.name.replaceAll(' ', '/'),
52
+ command.command_options.map(option => option.flags),
53
+ ]));
54
+ expect(Object.fromEntries(browserCommandCatalog.map(command => [
55
+ command.command,
56
+ command.options.map(browserOptionFlags),
57
+ ]))).toEqual(Object.fromEntries(local));
58
+ });
59
+ it('marks bind as the only local-only command and gives every hosted command an action', () => {
60
+ expect(browserCommandCatalog.filter(command => command.sessionPolicy === 'local-only').map(command => command.command))
61
+ .toEqual(['bind']);
62
+ expect(browserCommandCatalog.find(command => command.command === 'bind')).not.toHaveProperty('action');
63
+ expect(browserCommandCatalog.filter(command => command.command !== 'bind').every(command => command.action))
64
+ .toBe(true);
65
+ });
66
+ it('classifies lifecycle commands from their actual local behavior', () => {
67
+ const policies = Object.fromEntries(browserCommandCatalog.map(command => [command.command, command.sessionPolicy]));
68
+ expect(policies.open).toBe('create-or-reuse');
69
+ expect(policies.close).toBe('close-existing');
70
+ expect(policies.unbind).toBe('close-existing');
71
+ expect(policies.state).toBe('require-existing');
72
+ });
73
+ it('is the production browser surface emitted by buildHostedContract', () => {
74
+ const contract = buildHostedContract([], [], '1.0.0');
75
+ expect(contract.browserCommands).toEqual(browserCommandCatalog);
76
+ expect(contract.browserCommands.filter(command => !command.action).map(command => command.command))
77
+ .toEqual(['bind']);
78
+ });
79
+ });
@@ -0,0 +1,17 @@
1
+ import { buildLaunchOptions, humanizeBrowser } from 'cloakbrowser';
2
+ import type { LaunchPersistentContextOptions } from 'cloakbrowser';
3
+ import type { Browser, BrowserContext } from 'playwright-core';
4
+ type Dependencies = {
5
+ buildLaunchOptions: typeof buildLaunchOptions;
6
+ humanizeBrowser: typeof humanizeBrowser;
7
+ openApplication: (appPath: string, args: string[]) => Promise<void>;
8
+ activateApplication: (appPath: string) => Promise<void>;
9
+ readPort: (portFile: string) => Promise<number>;
10
+ connectOverCDP: (endpoint: string) => Promise<Browser>;
11
+ terminateProfile: (userDataDir: string) => Promise<void>;
12
+ removePortFile: (portFile: string) => Promise<void>;
13
+ };
14
+ export declare function waitForDevToolsPort(portFile: string, timeoutMs?: number): Promise<number>;
15
+ export declare function activateDarwinBackgroundContext(context: BrowserContext): Promise<void>;
16
+ export declare function launchDarwinBackgroundPersistentContext(options: LaunchPersistentContextOptions, deps?: Dependencies): Promise<BrowserContext>;
17
+ export {};
@@ -0,0 +1,106 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile, rm } from 'node:fs/promises';
3
+ import { posix as path } from 'node:path';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { promisify } from 'node:util';
6
+ import { buildLaunchOptions, humanizeBrowser } from 'cloakbrowser';
7
+ import { chromium } from 'playwright-core';
8
+ const execFileAsync = promisify(execFile);
9
+ async function openApplication(appPath, args) {
10
+ await execFileAsync('/usr/bin/open', ['-g', '-n', appPath, '--args', ...args]);
11
+ }
12
+ export async function waitForDevToolsPort(portFile, timeoutMs = 10_000) {
13
+ const deadline = Date.now() + timeoutMs;
14
+ while (Date.now() < deadline) {
15
+ try {
16
+ const port = Number.parseInt((await readFile(portFile, 'utf8')).split('\n')[0], 10);
17
+ if (Number.isInteger(port) && port > 0)
18
+ return port;
19
+ }
20
+ catch (error) {
21
+ if (error.code !== 'ENOENT')
22
+ throw error;
23
+ }
24
+ await delay(50);
25
+ }
26
+ throw new Error('Timed out waiting for background Chromium CDP endpoint');
27
+ }
28
+ async function terminateProfile(userDataDir) {
29
+ const { stdout } = await execFileAsync('/bin/ps', ['-axo', 'pid=,command=']);
30
+ const needle = `--user-data-dir=${userDataDir}`;
31
+ for (const line of stdout.split('\n')) {
32
+ if (!line.includes(needle))
33
+ continue;
34
+ const pid = Number.parseInt(line.trim().split(/\s+/, 1)[0], 10);
35
+ if (Number.isInteger(pid) && pid !== process.pid)
36
+ process.kill(pid, 'SIGTERM');
37
+ }
38
+ }
39
+ const defaultDependencies = {
40
+ buildLaunchOptions,
41
+ humanizeBrowser,
42
+ openApplication,
43
+ activateApplication: async (appPath) => {
44
+ await execFileAsync('/usr/bin/open', [appPath]);
45
+ },
46
+ readPort: waitForDevToolsPort,
47
+ connectOverCDP: endpoint => chromium.connectOverCDP(endpoint),
48
+ terminateProfile,
49
+ removePortFile: portFile => rm(portFile, { force: true }),
50
+ };
51
+ const contextActivators = new WeakMap();
52
+ export async function activateDarwinBackgroundContext(context) {
53
+ await contextActivators.get(context)?.();
54
+ }
55
+ function appPathFor(executablePath) {
56
+ const marker = `${path.sep}Contents${path.sep}MacOS${path.sep}`;
57
+ const index = executablePath.lastIndexOf(marker);
58
+ if (index < 0)
59
+ throw new Error(`Cloak Chromium executable is not inside a macOS app bundle: ${executablePath}`);
60
+ return executablePath.slice(0, index);
61
+ }
62
+ export async function launchDarwinBackgroundPersistentContext(options, deps = defaultDependencies) {
63
+ const portFile = path.join(options.userDataDir, 'DevToolsActivePort');
64
+ await deps.removePortFile(portFile);
65
+ const launchOptions = await deps.buildLaunchOptions(options);
66
+ if (!launchOptions.executablePath)
67
+ throw new Error('Cloak Chromium executable path is missing');
68
+ const appPath = appPathFor(launchOptions.executablePath);
69
+ let browser;
70
+ let launched = false;
71
+ try {
72
+ await deps.openApplication(appPath, [
73
+ ...(launchOptions.args ?? []),
74
+ '--password-store=basic',
75
+ '--use-mock-keychain',
76
+ `--user-data-dir=${options.userDataDir}`,
77
+ '--remote-debugging-address=127.0.0.1',
78
+ '--remote-debugging-port=0',
79
+ 'about:blank',
80
+ ]);
81
+ launched = true;
82
+ const port = await deps.readPort(portFile);
83
+ browser = await deps.connectOverCDP(`http://127.0.0.1:${port}`);
84
+ await deps.humanizeBrowser(browser, options);
85
+ const context = browser.contexts()[0];
86
+ if (!context)
87
+ throw new Error('Background Chromium did not expose a persistent context');
88
+ contextActivators.set(context, () => deps.activateApplication(appPath));
89
+ context.close = async () => {
90
+ try {
91
+ await browser.close();
92
+ }
93
+ finally {
94
+ contextActivators.delete(context);
95
+ await deps.terminateProfile(options.userDataDir);
96
+ }
97
+ };
98
+ return context;
99
+ }
100
+ catch (error) {
101
+ await browser?.close().catch(() => { });
102
+ if (launched)
103
+ await deps.terminateProfile(options.userDataDir).catch(() => { });
104
+ throw error;
105
+ }
106
+ }