@oomfware/cbr 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +72 -0
  3. package/dist/assets/system-prompt.md +147 -0
  4. package/dist/client.mjs +54 -0
  5. package/dist/index.mjs +1366 -0
  6. package/package.json +45 -0
  7. package/src/assets/system-prompt.md +147 -0
  8. package/src/client.ts +70 -0
  9. package/src/commands/ask.ts +202 -0
  10. package/src/commands/clean.ts +18 -0
  11. package/src/index.ts +34 -0
  12. package/src/lib/commands/_types.ts +24 -0
  13. package/src/lib/commands/_utils.ts +38 -0
  14. package/src/lib/commands/back.ts +14 -0
  15. package/src/lib/commands/check.ts +14 -0
  16. package/src/lib/commands/click.ts +14 -0
  17. package/src/lib/commands/close.ts +17 -0
  18. package/src/lib/commands/dblclick.ts +14 -0
  19. package/src/lib/commands/download.ts +36 -0
  20. package/src/lib/commands/eval.ts +23 -0
  21. package/src/lib/commands/fill.ts +18 -0
  22. package/src/lib/commands/forward.ts +14 -0
  23. package/src/lib/commands/frame.ts +106 -0
  24. package/src/lib/commands/get.ts +95 -0
  25. package/src/lib/commands/hover.ts +14 -0
  26. package/src/lib/commands/is.ts +53 -0
  27. package/src/lib/commands/open.ts +15 -0
  28. package/src/lib/commands/press.ts +13 -0
  29. package/src/lib/commands/reload.ts +14 -0
  30. package/src/lib/commands/resources.ts +37 -0
  31. package/src/lib/commands/screenshot.ts +26 -0
  32. package/src/lib/commands/scroll.ts +30 -0
  33. package/src/lib/commands/select.ts +18 -0
  34. package/src/lib/commands/snapshot.ts +30 -0
  35. package/src/lib/commands/source.ts +23 -0
  36. package/src/lib/commands/styles.ts +63 -0
  37. package/src/lib/commands/tab.ts +102 -0
  38. package/src/lib/commands/type-text.ts +18 -0
  39. package/src/lib/commands/uncheck.ts +14 -0
  40. package/src/lib/commands/wait.ts +93 -0
  41. package/src/lib/commands.ts +202 -0
  42. package/src/lib/debug.ts +11 -0
  43. package/src/lib/paths.ts +118 -0
  44. package/src/lib/server.ts +94 -0
  45. package/src/lib/session.ts +92 -0
  46. package/src/lib/snapshot.ts +351 -0
@@ -0,0 +1,36 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ import { argument, constant, type InferValue, message, object, string } from '@optique/core';
5
+ import { optional } from '@optique/core/modifiers';
6
+
7
+ import { CommandError, type BrowserState } from './_types.ts';
8
+ import { filenameFromUrl, formatBytes } from './_utils.ts';
9
+
10
+ export const schema = object({
11
+ command: constant('download'),
12
+ url: argument(string({ metavar: 'URL' }), { description: message`URL of the resource to download` }),
13
+ filename: optional(
14
+ argument(string({ metavar: 'FILENAME' }), { description: message`save as this filename` }),
15
+ ),
16
+ });
17
+
18
+ export type Args = InferValue<typeof schema>;
19
+
20
+ export const handler = async (state: BrowserState, args: Args): Promise<string> => {
21
+ const filename = args.filename ?? filenameFromUrl(args.url);
22
+
23
+ const start = performance.now();
24
+ const response = await state.page.request.get(args.url);
25
+ if (!response.ok()) {
26
+ throw new CommandError(`download failed: ${response.status()} ${response.statusText()}`);
27
+ }
28
+
29
+ const buffer = await response.body();
30
+ const filepath = join(state.assetsDir, filename);
31
+ await writeFile(filepath, buffer);
32
+
33
+ const size = formatBytes(buffer.length);
34
+ const elapsed = ((performance.now() - start) / 1000).toFixed(1);
35
+ return `saved assets/${filename} (${size}, ${elapsed}s)`;
36
+ };
@@ -0,0 +1,23 @@
1
+ import { constant, type InferValue, message, object, passThrough } from '@optique/core';
2
+
3
+ import { type BrowserState, CommandError } from './_types.ts';
4
+
5
+ export const schema = object({
6
+ command: constant('eval'),
7
+ code: passThrough({ format: 'greedy', description: message`JavaScript code to evaluate` }),
8
+ });
9
+
10
+ export type Args = InferValue<typeof schema>;
11
+
12
+ export const handler = async (state: BrowserState, args: Args): Promise<string | undefined> => {
13
+ const code = args.code.join(' ');
14
+ if (!code) {
15
+ throw new CommandError('missing code to evaluate');
16
+ }
17
+
18
+ const result = await state.page.evaluate(code);
19
+ if (result === undefined || result === null) {
20
+ return undefined;
21
+ }
22
+ return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
23
+ };
@@ -0,0 +1,18 @@
1
+ import { argument, constant, message, object, string } from '@optique/core';
2
+
3
+ import type { BrowserState } from './_types.ts';
4
+ import { getLocator } from './_utils.ts';
5
+
6
+ export const schema = object({
7
+ command: constant('fill'),
8
+ selector: argument(string({ metavar: 'SELECTOR' }), { description: message`input element to fill` }),
9
+ text: argument(string({ metavar: 'TEXT' }), { description: message`text to fill in` }),
10
+ });
11
+
12
+ export const handler = async (
13
+ state: BrowserState,
14
+ args: { selector: string; text: string },
15
+ ): Promise<string> => {
16
+ await getLocator(state, args.selector).fill(args.text);
17
+ return 'filled';
18
+ };
@@ -0,0 +1,14 @@
1
+ import { constant, object } from '@optique/core';
2
+
3
+ import type { BrowserState } from './_types.ts';
4
+
5
+ export const schema = object({
6
+ command: constant('forward'),
7
+ });
8
+
9
+ export const handler = async (state: BrowserState): Promise<string> => {
10
+ const start = performance.now();
11
+ await state.page.goForward({ waitUntil: 'domcontentloaded' });
12
+ const elapsed = ((performance.now() - start) / 1000).toFixed(1);
13
+ return `navigated forward to ${state.page.url()} (${elapsed}s)`;
14
+ };
@@ -0,0 +1,106 @@
1
+ import { argument, command, constant, type InferValue, message, object, or, string } from '@optique/core';
2
+ import type { Frame, Page } from 'playwright';
3
+
4
+ import { type BrowserState, CommandError } from './_types.ts';
5
+
6
+ export const schema = object({
7
+ command: constant('frame'),
8
+ subcommand: or(
9
+ command(
10
+ 'list',
11
+ object({
12
+ kind: constant('list'),
13
+ }),
14
+ { description: message`list all frames with IDs, URLs, and parent info` },
15
+ ),
16
+ command(
17
+ 'main',
18
+ object({
19
+ kind: constant('main'),
20
+ }),
21
+ { description: message`switch to the main frame` },
22
+ ),
23
+ // `frame <id>` — switch by frame ref (positional, no subcommand keyword)
24
+ object({
25
+ kind: constant('switch'),
26
+ id: argument(string({ metavar: 'FRAME_ID' }), { description: message`frame ref to switch to` }),
27
+ }),
28
+ ),
29
+ });
30
+
31
+ export type Args = InferValue<typeof schema>;
32
+
33
+ /**
34
+ * collects child frames from the page, assigns IDs (`f1`, `f2`, ...), and
35
+ * stores them in `state.frameRefs`. the main frame is excluded since
36
+ * `frame main` handles switching back to it.
37
+ */
38
+ const refreshFrameRefs = (state: BrowserState): Record<string, Frame> => {
39
+ const mainPage = state.context.pages()[0]!;
40
+ const mainFrame = mainPage.mainFrame();
41
+ const childFrames = mainPage.frames().filter((f) => f !== mainFrame);
42
+ const map: Record<string, Frame> = {};
43
+ for (let i = 0; i < childFrames.length; i++) {
44
+ map[`f${i + 1}`] = childFrames[i]!;
45
+ }
46
+ state.frameRefs = map;
47
+ return map;
48
+ };
49
+
50
+ const formatFrameList = (refs: Record<string, Frame>, currentPage: Page): string => {
51
+ const mainFrame = currentPage.mainFrame();
52
+ const currentFrame = currentPage as unknown as Frame;
53
+ const lines: string[] = [];
54
+
55
+ {
56
+ const marker = currentFrame === mainFrame ? '* ' : ' ';
57
+ lines.push(`${marker}main: ${mainFrame.url()}`);
58
+ }
59
+
60
+ for (const [id, frame] of Object.entries(refs)) {
61
+ const marker = frame === currentFrame ? '* ' : ' ';
62
+ const name = frame.name() ? ` name="${frame.name()}"` : '';
63
+
64
+ let parentTag = '';
65
+ {
66
+ const parent = frame.parentFrame();
67
+ if (parent === mainFrame) {
68
+ parentTag = ' (parent: main)';
69
+ } else if (parent) {
70
+ const parentId = Object.entries(refs).find(([, f]) => f === parent)?.[0];
71
+ if (parentId) {
72
+ parentTag = ` (parent: ${parentId})`;
73
+ }
74
+ }
75
+ }
76
+
77
+ lines.push(`${marker}${id}: ${frame.url()}${name}${parentTag}`);
78
+ }
79
+ return lines.join('\n');
80
+ };
81
+
82
+ export const handler = async (state: BrowserState, args: Args): Promise<string> => {
83
+ const sub = args.subcommand;
84
+
85
+ switch (sub.kind) {
86
+ case 'list': {
87
+ const refs = refreshFrameRefs(state);
88
+ return formatFrameList(refs, state.page);
89
+ }
90
+ case 'main': {
91
+ state.page = state.context.pages()[0]!;
92
+ return 'switched to main frame';
93
+ }
94
+ case 'switch': {
95
+ const frame = state.frameRefs[sub.id];
96
+ if (!frame) {
97
+ throw new CommandError(
98
+ `frame ${sub.id} not found — run \`browser frame list\` to list frames and get IDs`,
99
+ );
100
+ }
101
+ // Frame implements a subset of Page's API that we use
102
+ state.page = frame as unknown as Page;
103
+ return `switched to frame ${sub.id}: ${frame.url()}`;
104
+ }
105
+ }
106
+ };
@@ -0,0 +1,95 @@
1
+ import { argument, command, constant, type InferValue, message, object, or, string } from '@optique/core';
2
+
3
+ import type { BrowserState } from './_types.ts';
4
+ import { getLocator } from './_utils.ts';
5
+
6
+ export const schema = object({
7
+ command: constant('get'),
8
+ subcommand: or(
9
+ command(
10
+ 'text',
11
+ object({
12
+ kind: constant('text'),
13
+ selector: argument(string({ metavar: 'SELECTOR' })),
14
+ }),
15
+ { description: message`get inner text of an element` },
16
+ ),
17
+ command(
18
+ 'url',
19
+ object({
20
+ kind: constant('url'),
21
+ }),
22
+ { description: message`get the current page URL` },
23
+ ),
24
+ command(
25
+ 'title',
26
+ object({
27
+ kind: constant('title'),
28
+ }),
29
+ { description: message`get the current page title` },
30
+ ),
31
+ command(
32
+ 'html',
33
+ object({
34
+ kind: constant('html'),
35
+ selector: argument(string({ metavar: 'SELECTOR' })),
36
+ }),
37
+ { description: message`get inner HTML of an element` },
38
+ ),
39
+ command(
40
+ 'value',
41
+ object({
42
+ kind: constant('value'),
43
+ selector: argument(string({ metavar: 'SELECTOR' })),
44
+ }),
45
+ { description: message`get the value of an input field` },
46
+ ),
47
+ command(
48
+ 'attr',
49
+ object({
50
+ kind: constant('attr'),
51
+ selector: argument(string({ metavar: 'SELECTOR' })),
52
+ attribute: argument(string({ metavar: 'ATTR' })),
53
+ }),
54
+ { description: message`get an attribute of an element` },
55
+ ),
56
+ command(
57
+ 'count',
58
+ object({
59
+ kind: constant('count'),
60
+ selector: argument(string({ metavar: 'SELECTOR' })),
61
+ }),
62
+ { description: message`count matching elements` },
63
+ ),
64
+ ),
65
+ });
66
+
67
+ export type Args = InferValue<typeof schema>;
68
+
69
+ export const handler = async (state: BrowserState, args: Args): Promise<string> => {
70
+ switch (args.subcommand.kind) {
71
+ case 'text': {
72
+ return await getLocator(state, args.subcommand.selector).innerText();
73
+ }
74
+ case 'url': {
75
+ return state.page.url();
76
+ }
77
+ case 'title': {
78
+ return await state.page.title();
79
+ }
80
+ case 'html': {
81
+ return await getLocator(state, args.subcommand.selector).innerHTML();
82
+ }
83
+ case 'value': {
84
+ return await getLocator(state, args.subcommand.selector).inputValue();
85
+ }
86
+ case 'attr': {
87
+ const value = await getLocator(state, args.subcommand.selector).getAttribute(args.subcommand.attribute);
88
+ return value ?? '';
89
+ }
90
+ case 'count': {
91
+ const count = await getLocator(state, args.subcommand.selector).count();
92
+ return String(count);
93
+ }
94
+ }
95
+ };
@@ -0,0 +1,14 @@
1
+ import { argument, constant, object, string } from '@optique/core';
2
+
3
+ import type { BrowserState } from './_types.ts';
4
+ import { getLocator } from './_utils.ts';
5
+
6
+ export const schema = object({
7
+ command: constant('hover'),
8
+ selector: argument(string({ metavar: 'SELECTOR' })),
9
+ });
10
+
11
+ export const handler = async (state: BrowserState, args: { selector: string }): Promise<string> => {
12
+ await getLocator(state, args.selector).hover();
13
+ return 'hovered';
14
+ };
@@ -0,0 +1,53 @@
1
+ import { argument, command, constant, type InferValue, message, object, or, string } from '@optique/core';
2
+
3
+ import type { BrowserState } from './_types.ts';
4
+ import { getLocator } from './_utils.ts';
5
+
6
+ export const schema = object({
7
+ command: constant('is'),
8
+ subcommand: or(
9
+ command(
10
+ 'visible',
11
+ object({
12
+ kind: constant('visible'),
13
+ selector: argument(string({ metavar: 'SELECTOR' })),
14
+ }),
15
+ { description: message`check if an element is visible` },
16
+ ),
17
+ command(
18
+ 'enabled',
19
+ object({
20
+ kind: constant('enabled'),
21
+ selector: argument(string({ metavar: 'SELECTOR' })),
22
+ }),
23
+ { description: message`check if an element is enabled` },
24
+ ),
25
+ command(
26
+ 'checked',
27
+ object({
28
+ kind: constant('checked'),
29
+ selector: argument(string({ metavar: 'SELECTOR' })),
30
+ }),
31
+ { description: message`check if a checkbox is checked` },
32
+ ),
33
+ ),
34
+ });
35
+
36
+ export type Args = InferValue<typeof schema>;
37
+
38
+ export const handler = async (state: BrowserState, args: Args): Promise<string> => {
39
+ switch (args.subcommand.kind) {
40
+ case 'visible': {
41
+ const visible = await getLocator(state, args.subcommand.selector).isVisible();
42
+ return String(visible);
43
+ }
44
+ case 'enabled': {
45
+ const enabled = await getLocator(state, args.subcommand.selector).isEnabled();
46
+ return String(enabled);
47
+ }
48
+ case 'checked': {
49
+ const checked = await getLocator(state, args.subcommand.selector).isChecked();
50
+ return String(checked);
51
+ }
52
+ }
53
+ };
@@ -0,0 +1,15 @@
1
+ import { argument, constant, object, string } from '@optique/core';
2
+
3
+ import type { BrowserState } from './_types.ts';
4
+
5
+ export const schema = object({
6
+ command: constant('open'),
7
+ url: argument(string({ metavar: 'URL' })),
8
+ });
9
+
10
+ export const handler = async (state: BrowserState, args: { url: string }): Promise<string> => {
11
+ const start = performance.now();
12
+ await state.page.goto(args.url, { waitUntil: 'domcontentloaded' });
13
+ const elapsed = ((performance.now() - start) / 1000).toFixed(1);
14
+ return `navigated to ${state.page.url()} (${elapsed}s)`;
15
+ };
@@ -0,0 +1,13 @@
1
+ import { argument, constant, object, string } from '@optique/core';
2
+
3
+ import type { BrowserState } from './_types.ts';
4
+
5
+ export const schema = object({
6
+ command: constant('press'),
7
+ key: argument(string({ metavar: 'KEY' })),
8
+ });
9
+
10
+ export const handler = async (state: BrowserState, args: { key: string }): Promise<string> => {
11
+ await state.page.keyboard.press(args.key);
12
+ return `pressed ${args.key}`;
13
+ };
@@ -0,0 +1,14 @@
1
+ import { constant, object } from '@optique/core';
2
+
3
+ import type { BrowserState } from './_types.ts';
4
+
5
+ export const schema = object({
6
+ command: constant('reload'),
7
+ });
8
+
9
+ export const handler = async (state: BrowserState): Promise<string> => {
10
+ const start = performance.now();
11
+ await state.page.reload({ waitUntil: 'domcontentloaded' });
12
+ const elapsed = ((performance.now() - start) / 1000).toFixed(1);
13
+ return `reloaded ${state.page.url()} (${elapsed}s)`;
14
+ };
@@ -0,0 +1,37 @@
1
+ import { argument, constant, type InferValue, message, object, string } from '@optique/core';
2
+ import { optional } from '@optique/core/modifiers';
3
+
4
+ import type { BrowserState } from './_types.ts';
5
+ import { formatBytes } from './_utils.ts';
6
+
7
+ export const schema = object({
8
+ command: constant('resources'),
9
+ typeFilter: optional(
10
+ argument(string({ metavar: 'TYPE' }), {
11
+ description: message`filter by resource type (e.g. script, img)`,
12
+ }),
13
+ ),
14
+ });
15
+
16
+ export type Args = InferValue<typeof schema>;
17
+
18
+ export const handler = async (state: BrowserState, args: Args): Promise<string> => {
19
+ const entries: Array<{ name: string; type: string; size: number }> = await state.page.evaluate(() =>
20
+ performance.getEntriesByType('resource').map((e) => {
21
+ const r = e as PerformanceResourceTiming;
22
+ return { name: r.name, type: r.initiatorType, size: r.transferSize };
23
+ }),
24
+ );
25
+
26
+ const filtered = args.typeFilter ? entries.filter((e) => e.type === args.typeFilter) : entries;
27
+
28
+ if (filtered.length === 0) {
29
+ return args.typeFilter ? `no resources of type "${args.typeFilter}"` : 'no resources recorded';
30
+ }
31
+
32
+ const lines = filtered.map((e) => {
33
+ const size = e.size > 0 ? ` (${formatBytes(e.size)})` : '';
34
+ return `[${e.type}] ${e.name}${size}`;
35
+ });
36
+ return lines.join('\n');
37
+ };
@@ -0,0 +1,26 @@
1
+ import { join } from 'node:path';
2
+
3
+ import { argument, constant, type InferValue, message, object, option, string } from '@optique/core';
4
+ import { optional, withDefault } from '@optique/core/modifiers';
5
+
6
+ import type { BrowserState } from './_types.ts';
7
+
8
+ export const schema = object({
9
+ command: constant('screenshot'),
10
+ name: optional(
11
+ argument(string({ metavar: 'NAME' }), { description: message`filename for the screenshot` }),
12
+ ),
13
+ full: withDefault(option('--full', { description: message`capture the full scrollable page` }), false),
14
+ });
15
+
16
+ export type Args = InferValue<typeof schema>;
17
+
18
+ export const handler = async (state: BrowserState, args: Args): Promise<string> => {
19
+ state.screenshotCounter++;
20
+ const name = args.name ?? `screenshot-${state.screenshotCounter}`;
21
+ const filename = name.endsWith('.png') ? name : `${name}.png`;
22
+ const filepath = join(state.screenshotDir, filename);
23
+
24
+ await state.page.screenshot({ path: filepath, fullPage: args.full });
25
+ return `screenshot saved to screenshots/${filename}`;
26
+ };
@@ -0,0 +1,30 @@
1
+ import { argument, choice, constant, type InferValue, message, object, string } from '@optique/core';
2
+ import { optional } from '@optique/core/modifiers';
3
+
4
+ import type { BrowserState } from './_types.ts';
5
+ import { getLocator } from './_utils.ts';
6
+
7
+ export const schema = object({
8
+ command: constant('scroll'),
9
+ direction: argument(choice(['up', 'down']), { description: message`scroll direction` }),
10
+ selector: optional(
11
+ argument(string({ metavar: 'SELECTOR' }), {
12
+ description: message`element to scroll instead of the page`,
13
+ }),
14
+ ),
15
+ });
16
+
17
+ export type Args = InferValue<typeof schema>;
18
+
19
+ export const handler = async (state: BrowserState, args: Args): Promise<string> => {
20
+ const delta = args.direction === 'down' ? 500 : -500;
21
+
22
+ if (args.selector) {
23
+ const locator = getLocator(state, args.selector);
24
+ await locator.evaluate((el, d) => el.scrollBy(0, d), delta);
25
+ } else {
26
+ await state.page.mouse.wheel(0, delta);
27
+ }
28
+
29
+ return `scrolled ${args.direction}`;
30
+ };
@@ -0,0 +1,18 @@
1
+ import { argument, constant, message, object, string } from '@optique/core';
2
+
3
+ import type { BrowserState } from './_types.ts';
4
+ import { getLocator } from './_utils.ts';
5
+
6
+ export const schema = object({
7
+ command: constant('select'),
8
+ selector: argument(string({ metavar: 'SELECTOR' }), { description: message`select element to target` }),
9
+ value: argument(string({ metavar: 'VALUE' }), { description: message`option value to select` }),
10
+ });
11
+
12
+ export const handler = async (
13
+ state: BrowserState,
14
+ args: { selector: string; value: string },
15
+ ): Promise<string> => {
16
+ await getLocator(state, args.selector).selectOption(args.value);
17
+ return `selected ${args.value}`;
18
+ };
@@ -0,0 +1,30 @@
1
+ import { constant, type InferValue, integer, message, object, option, string } from '@optique/core';
2
+ import { optional, withDefault } from '@optique/core/modifiers';
3
+
4
+ import { takeSnapshot } from '../snapshot.ts';
5
+
6
+ import type { BrowserState } from './_types.ts';
7
+
8
+ export const schema = object({
9
+ command: constant('snapshot'),
10
+ interactive: withDefault(
11
+ option('--interactive', { description: message`only show interactive elements` }),
12
+ false,
13
+ ),
14
+ compact: withDefault(option('--compact', { description: message`remove empty lines from output` }), false),
15
+ depth: optional(option('--depth', integer({ min: 0 }), { description: message`maximum tree depth` })),
16
+ selector: optional(option('--selector', string(), { description: message`scope to a subtree` })),
17
+ });
18
+
19
+ export type Args = InferValue<typeof schema>;
20
+
21
+ export const handler = async (state: BrowserState, args: Args): Promise<string> => {
22
+ const result = await takeSnapshot(state.page, {
23
+ interactive: args.interactive || undefined,
24
+ compact: args.compact || undefined,
25
+ depth: args.depth ?? undefined,
26
+ selector: args.selector ?? undefined,
27
+ });
28
+ state.refs = result.refs;
29
+ return result.text;
30
+ };
@@ -0,0 +1,23 @@
1
+ import { argument, constant, type InferValue, message, object, string } from '@optique/core';
2
+ import { optional } from '@optique/core/modifiers';
3
+
4
+ import type { BrowserState } from './_types.ts';
5
+ import { getLocator } from './_utils.ts';
6
+
7
+ export const schema = object({
8
+ command: constant('source'),
9
+ selector: optional(
10
+ argument(string({ metavar: 'SELECTOR' }), {
11
+ description: message`element to get HTML for, or full page if omitted`,
12
+ }),
13
+ ),
14
+ });
15
+
16
+ export type Args = InferValue<typeof schema>;
17
+
18
+ export const handler = async (state: BrowserState, args: Args): Promise<string> => {
19
+ if (args.selector) {
20
+ return await getLocator(state, args.selector).evaluate((el) => el.outerHTML);
21
+ }
22
+ return await state.page.content();
23
+ };
@@ -0,0 +1,63 @@
1
+ import { argument, constant, type InferValue, message, object, string } from '@optique/core';
2
+ import { optional } from '@optique/core/modifiers';
3
+
4
+ import type { BrowserState } from './_types.ts';
5
+ import { getLocator } from './_utils.ts';
6
+
7
+ // browser evaluate callbacks run in the browser context where getComputedStyle exists
8
+ declare const getComputedStyle: (el: unknown) => { getPropertyValue(prop: string): string };
9
+
10
+ export const schema = object({
11
+ command: constant('styles'),
12
+ selector: argument(string({ metavar: 'SELECTOR' }), { description: message`element to inspect` }),
13
+ property: optional(
14
+ argument(string({ metavar: 'PROPERTY' }), {
15
+ description: message`specific CSS property, or all if omitted`,
16
+ }),
17
+ ),
18
+ });
19
+
20
+ export type Args = InferValue<typeof schema>;
21
+
22
+ export const handler = async (state: BrowserState, args: Args): Promise<string> => {
23
+ if (args.property) {
24
+ const value = await getLocator(state, args.selector).evaluate(
25
+ (el, prop) => getComputedStyle(el).getPropertyValue(prop),
26
+ args.property,
27
+ );
28
+ return value || '(empty)';
29
+ }
30
+
31
+ // return a curated set of commonly useful properties
32
+ const styles = await getLocator(state, args.selector).evaluate((el) => {
33
+ const cs = getComputedStyle(el);
34
+ const props = [
35
+ 'color',
36
+ 'background-color',
37
+ 'font-family',
38
+ 'font-size',
39
+ 'font-weight',
40
+ 'line-height',
41
+ 'display',
42
+ 'position',
43
+ 'width',
44
+ 'height',
45
+ 'margin',
46
+ 'padding',
47
+ 'border',
48
+ 'opacity',
49
+ 'z-index',
50
+ ];
51
+ const result: Record<string, string> = {};
52
+ for (const p of props) {
53
+ const v = cs.getPropertyValue(p);
54
+ if (v) {
55
+ result[p] = v;
56
+ }
57
+ }
58
+ return result;
59
+ });
60
+
61
+ const lines = Object.entries(styles).map(([k, v]) => `${k}: ${v}`);
62
+ return lines.join('\n');
63
+ };