cssprobe-cli 0.3.0 → 0.3.1
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/AGENTS.md +29 -4
- package/README.md +60 -1
- package/dist/browser/interactions.d.ts +48 -0
- package/dist/browser/interactions.js +88 -0
- package/dist/cli/commands.js +71 -0
- package/dist/cli/program.js +19 -0
- package/dist/cli/session-ops.d.ts +2 -1
- package/dist/cli/session-ops.js +3 -55
- package/dist/daemon/daemon.js +102 -6
- package/dist/daemon/flowRunner.d.ts +62 -0
- package/dist/daemon/flowRunner.js +155 -0
- package/dist/engine/evalSerializer.d.ts +1 -0
- package/dist/engine/evalSerializer.js +43 -0
- package/dist/help.json +63 -1
- package/dist/installer/skill-template.js +43 -0
- package/dist/utils/screenshot.d.ts +1 -0
- package/dist/utils/screenshot.js +58 -0
- package/llms.txt +1 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`cssprobe-cli` — Runtime CSS probe for layout/scroll/overflow/color/font inspection with session-based browser management. Entrypoint: `bin/cssprobe-cli.js` → `require('../dist/index')`.
|
|
6
6
|
Build: `npm run build` (= `tsc` + `esbuild` collector IIFE bundle + `esbuild` daemon entry + `tsx scripts/generate-help.ts`).
|
|
7
|
-
Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner,
|
|
7
|
+
Test: `npm test` (= `tsx --test tests/*.test.ts` — Node.js built-in test runner, 108 tests).
|
|
8
8
|
Dependencies: `playwright-core`, `zod`. Dev: `typescript`, `@types/node`, `tsx`, `esbuild`.
|
|
9
9
|
|
|
10
10
|
## Architecture
|
|
@@ -21,19 +21,23 @@ src/
|
|
|
21
21
|
├── daemon/
|
|
22
22
|
│ ├── session.ts # Session management (single session, Unix socket)
|
|
23
23
|
│ ├── daemon.ts # Daemon process: browser lifecycle + command execution
|
|
24
|
+
│ ├── flowRunner.ts # Flow script loader/runner (run command): dynamic import, API injection, console capture
|
|
24
25
|
│ └── daemonEntry.ts # Daemon entry point (spawned as child process)
|
|
25
26
|
├── engine/
|
|
26
27
|
│ ├── types.ts # Shared types: Snapshot, TreeNode, Finding, Confidence
|
|
27
28
|
│ ├── collector.ts # Browser-side: DOM traversal, computed styles, declared values
|
|
28
29
|
│ ├── analyzer.ts # Node-side: pure functions, confidence model, pattern detection
|
|
30
|
+
│ ├── evalSerializer.ts # Self-contained serializer for eval results (runs in-page via JSHandle.evaluate)
|
|
29
31
|
│ └── renderer.ts # Markdown / JSON rendering
|
|
30
32
|
├── browser/
|
|
31
|
-
│
|
|
33
|
+
│ ├── launcher.ts # Playwright wrapper: launch, navigate, inject collector
|
|
34
|
+
│ └── interactions.ts # Interaction primitives: click/hover/type/wait-for (shared by daemon + run API)
|
|
32
35
|
├── config/
|
|
33
36
|
│ ├── config.ts # Config loader (CLI → env → ~/.cssprobe-clirc profiles)
|
|
34
37
|
│ └── helpGenerator.ts # Build-time: Zod schemas → dist/help.json
|
|
35
38
|
└── utils/
|
|
36
|
-
|
|
39
|
+
├── socketConnection.ts # Socket communication (adapted from playwright-cli)
|
|
40
|
+
└── screenshot.ts # Screenshot data-URL → file saving (shared by CLI + flow runner)
|
|
37
41
|
```
|
|
38
42
|
|
|
39
43
|
## Key Patterns
|
|
@@ -73,11 +77,32 @@ src/
|
|
|
73
77
|
- `resize <width> <height>` — Resize browser viewport
|
|
74
78
|
- `eval <expression>` — Evaluate JavaScript (browser context)
|
|
75
79
|
- Returns raw string values (not JSON-quoted) for string results
|
|
80
|
+
- DOM nodes / circular structures are auto-serialized (descriptors, `[Circular]`, `[Function: name]`, depth cap 6)
|
|
76
81
|
- `playwright <call>` — Execute Playwright API call (Node.js context)
|
|
77
82
|
- Returns `{ ok: true, value: null, note: ... }` when the call returns no value; throws a clear error message on failure
|
|
78
83
|
- `screenshot` — Take a screenshot
|
|
79
84
|
- `--out <path>` — Output file path (default: `~/.cssprobe-cli/screenshots/<timestamp>.png`)
|
|
80
85
|
|
|
86
|
+
### Interaction
|
|
87
|
+
- `click <selector>` — Real mouse click (auto-scroll + actionability wait, works with custom React components)
|
|
88
|
+
- `--nth <n>` — 1-based match index (default: first match; multiple matches without `--nth` fail — strict)
|
|
89
|
+
- `--force` — skip actionability checks
|
|
90
|
+
- `--timeout <ms>` — action timeout (default 30000)
|
|
91
|
+
- `hover <selector>` — Real mouse hover (`--nth` / `--force` / `--timeout` as above)
|
|
92
|
+
- `type <selector> <text>` — Type text
|
|
93
|
+
- Default: `fill()` (replaces the value); `--sequential` for real key events (custom editors), `--clear` clears first
|
|
94
|
+
- `--press <key>` — press a key after typing (e.g. `Enter`)
|
|
95
|
+
- `--delay <ms>` — keystroke delay for `--sequential`
|
|
96
|
+
- `wait-for` — Wait for a condition (three mutually exclusive modes)
|
|
97
|
+
- `wait-for <selector>` — selector state: `--state visible|hidden|attached|detached` (default visible)
|
|
98
|
+
- `wait-for --js "<predicate>"` — wait until a JS predicate is truthy
|
|
99
|
+
- `wait-for --network-idle` — wait until the page is network-idle
|
|
100
|
+
- `--timeout <ms>` — default 30000; timeout errors name the target
|
|
101
|
+
- `run <file>` — Run a multi-step flow script inside the daemon
|
|
102
|
+
- Script default-exports an async function receiving the API object: `page`, `click`, `hover`, `type`, `waitFor`, `inspect`, `screenshot`, `log` (CJS also supported)
|
|
103
|
+
- `--timeout <ms>` — whole-script guard (default 300000, 0 = unlimited); failure exits 1 and keeps the session alive
|
|
104
|
+
- `screenshot` relative paths resolve against the script's directory; logs collected and returned
|
|
105
|
+
|
|
81
106
|
### State
|
|
82
107
|
- `state-import [file]` — Import cookies from Netscape format
|
|
83
108
|
- `--out <file>` — Output state file path (default: `~/.cssprobe-cli/states/imported.json`)
|
|
@@ -151,6 +176,6 @@ cssprobe-cli close
|
|
|
151
176
|
| Output | Single markdown string | Markdown (default) / JSON (--json) |
|
|
152
177
|
| Config | `window.__DOM_REPORT_CFG` global + eval two-step | CLI flags → env → rc file, one command |
|
|
153
178
|
| ROOT_SELECTOR | Manual or eval probe | `--auto-root` auto-detection |
|
|
154
|
-
| Tests | None | 45 unit tests (command, analyzer, output, renderer) |
|
|
179
|
+
| Tests | None | 45+ unit tests (command, analyzer, output, renderer) |
|
|
155
180
|
| Build | Regex stripping CJS module.exports | esbuild IIFE + CJS bundle, clean build |
|
|
156
181
|
| Session | None (blocking commands) | Session-based daemon architecture |
|
package/README.md
CHANGED
|
@@ -182,10 +182,24 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
|
|
|
182
182
|
| Command | Description |
|
|
183
183
|
|---------|-------------|
|
|
184
184
|
| `resize <width> <height>` | Resize browser viewport |
|
|
185
|
-
| `eval <expression>` | Evaluate JavaScript (browser context) |
|
|
185
|
+
| `eval <expression>` | Evaluate JavaScript (browser context); DOM nodes are auto-serialized |
|
|
186
186
|
| `playwright <call>` | Execute Playwright API (Node.js context) |
|
|
187
187
|
| `screenshot` | Take screenshot (saves to ~/.cssprobe-cli/screenshots/, --out to override) |
|
|
188
188
|
|
|
189
|
+
### Interaction
|
|
190
|
+
|
|
191
|
+
| Command | Description |
|
|
192
|
+
|---------|-------------|
|
|
193
|
+
| `click <selector>` | Real mouse click (auto-scroll + actionability wait; works with custom React components) |
|
|
194
|
+
| `hover <selector>` | Real mouse hover |
|
|
195
|
+
| `type <selector> <text>` | Fill text (replaces value); `--press Enter` to submit; `--sequential` for real key events |
|
|
196
|
+
| `wait-for <selector>` | Wait for selector state (`--state visible\|hidden\|attached\|detached`, `--timeout ms`) |
|
|
197
|
+
| `wait-for --js "<predicate>"` | Wait until a JS predicate is truthy |
|
|
198
|
+
| `wait-for --network-idle` | Wait until the page is network-idle |
|
|
199
|
+
| `run <file>` | Run a multi-step flow script |
|
|
200
|
+
|
|
201
|
+
`click` / `hover` support `--nth N` (1-based, default first match), `--force` (skip actionability checks) and `--timeout ms`. Without `--nth`, multiple matches fail (strict) to prevent wrong-target clicks.
|
|
202
|
+
|
|
189
203
|
### State
|
|
190
204
|
|
|
191
205
|
| Command | Description |
|
|
@@ -206,6 +220,51 @@ Exposes 10 tools: `cssprobe_open`, `cssprobe_inspect`, `cssprobe_tree`, `cssprob
|
|
|
206
220
|
|
|
207
221
|
---
|
|
208
222
|
|
|
223
|
+
## Multi-step Flows
|
|
224
|
+
|
|
225
|
+
The interaction commands drive a page step by step; `run` turns a sequence into a versionable script:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
cssprobe-cli open https://example.com/editor
|
|
229
|
+
cssprobe-cli wait-for .editor-canvas
|
|
230
|
+
cssprobe-cli click ".layout-switch" --nth 1
|
|
231
|
+
cssprobe-cli type ".block-title input" "Hello" --press Enter
|
|
232
|
+
cssprobe-cli inspect .block-grid --json
|
|
233
|
+
cssprobe-cli close
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
For repeatable flows, write a script and run it with one command. The script runs inside the daemon session (full Playwright `page` access):
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
// flow.js
|
|
240
|
+
export default async ({ page, click, waitFor, type, inspect, screenshot, log }) => {
|
|
241
|
+
await page.goto('https://example.com/editor', { waitUntil: 'domcontentloaded' });
|
|
242
|
+
await waitFor('.editor-canvas', { state: 'visible', timeout: 10000 });
|
|
243
|
+
|
|
244
|
+
await click('.layout-switch', { nth: 1 });
|
|
245
|
+
await waitFor({ js: "() => document.body.dataset.layout === 'two-col'" });
|
|
246
|
+
|
|
247
|
+
await type('.block-title input', 'Hello', { press: 'Enter', sequential: true, clear: true });
|
|
248
|
+
await waitFor({ networkIdle: true });
|
|
249
|
+
|
|
250
|
+
const { findings } = await inspect('.block-grid', { format: 'json' });
|
|
251
|
+
log('findings:', findings.length);
|
|
252
|
+
await screenshot({ path: './after-layout-switch.png' });
|
|
253
|
+
|
|
254
|
+
return { findings: findings.length };
|
|
255
|
+
};
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
cssprobe-cli run ./flow.js # --timeout ms for the whole script (default 300000, 0 = unlimited)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Scripts must `default-export` an async function receiving the API object: `page`, `click`, `hover`, `type`, `waitFor`, `inspect`, `screenshot`, `log`. `screenshot` relative paths resolve against the script's directory; logs (`log()` and `console.*`) are collected and returned with the result. On failure the command exits `1`, prints the collected logs, and keeps the session alive.
|
|
263
|
+
|
|
264
|
+
> Scripts are executed by the local daemon with full Node privileges — only run trusted scripts.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
209
268
|
## Confidence Model
|
|
210
269
|
|
|
211
270
|
Every finding carries a confidence level:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
export interface ElementDescriptor {
|
|
3
|
+
tag: string;
|
|
4
|
+
classes: string[];
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TargetResult {
|
|
8
|
+
matches: number;
|
|
9
|
+
nth: number;
|
|
10
|
+
descriptor: ElementDescriptor | null;
|
|
11
|
+
}
|
|
12
|
+
export type WaitState = 'visible' | 'hidden' | 'attached' | 'detached';
|
|
13
|
+
export type WaitTarget = {
|
|
14
|
+
kind: 'selector';
|
|
15
|
+
selector: string;
|
|
16
|
+
state: WaitState;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'js';
|
|
19
|
+
expression: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'networkIdle';
|
|
22
|
+
};
|
|
23
|
+
export declare function clickElement(page: Page, selector: string, opts?: {
|
|
24
|
+
nth?: number;
|
|
25
|
+
force?: boolean;
|
|
26
|
+
timeout?: number;
|
|
27
|
+
}): Promise<TargetResult>;
|
|
28
|
+
export declare function hoverElement(page: Page, selector: string, opts?: {
|
|
29
|
+
nth?: number;
|
|
30
|
+
force?: boolean;
|
|
31
|
+
timeout?: number;
|
|
32
|
+
}): Promise<TargetResult>;
|
|
33
|
+
export interface TypeOptions {
|
|
34
|
+
press?: string;
|
|
35
|
+
sequential?: boolean;
|
|
36
|
+
clear?: boolean;
|
|
37
|
+
delay?: number;
|
|
38
|
+
nth?: number;
|
|
39
|
+
timeout?: number;
|
|
40
|
+
}
|
|
41
|
+
export declare function typeInto(page: Page, selector: string, text: string, opts?: TypeOptions): Promise<void>;
|
|
42
|
+
export declare function describeWaitTarget(target: WaitTarget): string;
|
|
43
|
+
export declare function resolveWaitTarget(selector: string | undefined, opts: {
|
|
44
|
+
js?: string;
|
|
45
|
+
networkIdle?: boolean;
|
|
46
|
+
state?: string;
|
|
47
|
+
}): WaitTarget;
|
|
48
|
+
export declare function waitForCondition(page: Page, target: WaitTarget, timeout: number): Promise<number>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Interaction primitives shared by daemon commands and the run-script API.
|
|
3
|
+
// Strict mode is preserved when no nth is given: multiple matches fail the action.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.clickElement = clickElement;
|
|
6
|
+
exports.hoverElement = hoverElement;
|
|
7
|
+
exports.typeInto = typeInto;
|
|
8
|
+
exports.describeWaitTarget = describeWaitTarget;
|
|
9
|
+
exports.resolveWaitTarget = resolveWaitTarget;
|
|
10
|
+
exports.waitForCondition = waitForCondition;
|
|
11
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
12
|
+
function resolveLocator(page, selector, nth) {
|
|
13
|
+
const base = page.locator(selector);
|
|
14
|
+
return nth ? base.nth(nth - 1) : base;
|
|
15
|
+
}
|
|
16
|
+
async function describeTarget(locator) {
|
|
17
|
+
return await locator
|
|
18
|
+
.evaluate(el => ({
|
|
19
|
+
tag: el.tagName.toLowerCase(),
|
|
20
|
+
classes: Array.from(el.classList),
|
|
21
|
+
text: (el.textContent ?? '').trim().slice(0, 60),
|
|
22
|
+
}))
|
|
23
|
+
.catch(() => null);
|
|
24
|
+
}
|
|
25
|
+
async function interactTarget(page, selector, opts, action) {
|
|
26
|
+
const base = page.locator(selector);
|
|
27
|
+
const locator = resolveLocator(page, selector, opts.nth);
|
|
28
|
+
const matches = await base.count();
|
|
29
|
+
const descriptor = await describeTarget(locator);
|
|
30
|
+
await action(locator, { force: opts.force, timeout: opts.timeout ?? DEFAULT_TIMEOUT });
|
|
31
|
+
return { matches, nth: opts.nth ?? 1, descriptor };
|
|
32
|
+
}
|
|
33
|
+
async function clickElement(page, selector, opts = {}) {
|
|
34
|
+
return interactTarget(page, selector, opts, (locator, o) => locator.click({ force: o.force, timeout: o.timeout }));
|
|
35
|
+
}
|
|
36
|
+
async function hoverElement(page, selector, opts = {}) {
|
|
37
|
+
return interactTarget(page, selector, opts, (locator, o) => locator.hover({ force: o.force, timeout: o.timeout }));
|
|
38
|
+
}
|
|
39
|
+
async function typeInto(page, selector, text, opts = {}) {
|
|
40
|
+
const locator = resolveLocator(page, selector, opts.nth);
|
|
41
|
+
const timeout = opts.timeout ?? DEFAULT_TIMEOUT;
|
|
42
|
+
if (opts.sequential) {
|
|
43
|
+
if (opts.clear)
|
|
44
|
+
await locator.fill('', { timeout });
|
|
45
|
+
await locator.pressSequentially(text, { delay: opts.delay ?? 0, timeout });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
await locator.fill(text, { timeout });
|
|
49
|
+
}
|
|
50
|
+
if (opts.press)
|
|
51
|
+
await locator.press(opts.press, { timeout });
|
|
52
|
+
}
|
|
53
|
+
function describeWaitTarget(target) {
|
|
54
|
+
switch (target.kind) {
|
|
55
|
+
case 'selector':
|
|
56
|
+
return `${target.selector} (state: ${target.state})`;
|
|
57
|
+
case 'js':
|
|
58
|
+
return `JS predicate: ${target.expression.slice(0, 80)}`;
|
|
59
|
+
case 'networkIdle':
|
|
60
|
+
return 'network idle';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function resolveWaitTarget(selector, opts) {
|
|
64
|
+
const modeCount = [selector, opts.js, opts.networkIdle].filter(Boolean).length;
|
|
65
|
+
if (modeCount !== 1)
|
|
66
|
+
throw new Error('wait-for requires exactly one of: <selector>, --js "<expression>", --network-idle');
|
|
67
|
+
if (selector)
|
|
68
|
+
return { kind: 'selector', selector, state: (opts.state ?? 'visible') };
|
|
69
|
+
if (opts.js)
|
|
70
|
+
return { kind: 'js', expression: opts.js };
|
|
71
|
+
return { kind: 'networkIdle' };
|
|
72
|
+
}
|
|
73
|
+
async function waitForCondition(page, target, timeout) {
|
|
74
|
+
const started = Date.now();
|
|
75
|
+
switch (target.kind) {
|
|
76
|
+
case 'selector':
|
|
77
|
+
await page.waitForSelector(target.selector, { state: target.state, timeout });
|
|
78
|
+
break;
|
|
79
|
+
case 'js':
|
|
80
|
+
await page.waitForFunction(target.expression, null, { timeout });
|
|
81
|
+
break;
|
|
82
|
+
case 'networkIdle':
|
|
83
|
+
await page.waitForLoadState('networkidle', { timeout });
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
return Date.now() - started;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=interactions.js.map
|
package/dist/cli/commands.js
CHANGED
|
@@ -157,6 +157,75 @@ const playwrightCmd = (0, command_1.declareCommand)({
|
|
|
157
157
|
call: z.string().describe('Playwright API call, e.g. "page.setViewportSize({width: 1280, height: 720})"'),
|
|
158
158
|
}),
|
|
159
159
|
});
|
|
160
|
+
// ── interaction ──
|
|
161
|
+
const click = (0, command_1.declareCommand)({
|
|
162
|
+
name: 'click',
|
|
163
|
+
category: 'core',
|
|
164
|
+
description: 'Click an element with real mouse events (works with custom React components)',
|
|
165
|
+
args: z.object({
|
|
166
|
+
selector: z.string().describe('CSS selector'),
|
|
167
|
+
}),
|
|
168
|
+
options: z.object({
|
|
169
|
+
nth: numberArg.optional().describe('1-based match index (default: first match)'),
|
|
170
|
+
force: z.boolean().optional().describe('skip actionability checks'),
|
|
171
|
+
timeout: numberArg.optional().describe('action timeout in ms (default 30000)'),
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
const hover = (0, command_1.declareCommand)({
|
|
175
|
+
name: 'hover',
|
|
176
|
+
category: 'core',
|
|
177
|
+
description: 'Hover an element with real mouse events',
|
|
178
|
+
args: z.object({
|
|
179
|
+
selector: z.string().describe('CSS selector'),
|
|
180
|
+
}),
|
|
181
|
+
options: z.object({
|
|
182
|
+
nth: numberArg.optional().describe('1-based match index (default: first match)'),
|
|
183
|
+
force: z.boolean().optional().describe('skip actionability checks'),
|
|
184
|
+
timeout: numberArg.optional().describe('action timeout in ms (default 30000)'),
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
const typeCmd = (0, command_1.declareCommand)({
|
|
188
|
+
name: 'type',
|
|
189
|
+
category: 'core',
|
|
190
|
+
description: 'Type text into an element (fill by default; --sequential for real key events)',
|
|
191
|
+
args: z.object({
|
|
192
|
+
selector: z.string().describe('CSS selector'),
|
|
193
|
+
text: z.string().describe('text to type'),
|
|
194
|
+
}),
|
|
195
|
+
options: z.object({
|
|
196
|
+
press: z.string().optional().describe('key to press after typing (e.g. Enter, Tab, Control+A)'),
|
|
197
|
+
sequential: z.boolean().optional().describe('type with real key events (for custom editors)'),
|
|
198
|
+
clear: z.boolean().optional().describe('clear existing content before sequential typing'),
|
|
199
|
+
delay: numberArg.optional().describe('delay between keystrokes in ms (--sequential, default 0)'),
|
|
200
|
+
nth: numberArg.optional().describe('1-based match index (default: first match)'),
|
|
201
|
+
timeout: numberArg.optional().describe('action timeout in ms (default 30000)'),
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
const waitForCmd = (0, command_1.declareCommand)({
|
|
205
|
+
name: 'wait-for',
|
|
206
|
+
category: 'core',
|
|
207
|
+
description: 'Wait for a condition: selector state, JS predicate, or network idle',
|
|
208
|
+
args: z.object({
|
|
209
|
+
selector: z.string().optional().describe('CSS selector to wait for'),
|
|
210
|
+
}),
|
|
211
|
+
options: z.object({
|
|
212
|
+
state: z.enum(['visible', 'hidden', 'attached', 'detached']).optional().describe('selector state to wait for (default visible)'),
|
|
213
|
+
js: z.string().optional().describe('JS predicate, e.g. "() => document.querySelector(\'.x\')?.offsetHeight > 100"'),
|
|
214
|
+
'network-idle': z.boolean().optional().describe('wait until the page is network-idle'),
|
|
215
|
+
timeout: numberArg.optional().describe('timeout in ms (default 30000)'),
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
const runCmd = (0, command_1.declareCommand)({
|
|
219
|
+
name: 'run',
|
|
220
|
+
category: 'core',
|
|
221
|
+
description: 'Run a multi-step flow script (default-exports an async function receiving the API object)',
|
|
222
|
+
args: z.object({
|
|
223
|
+
file: z.string().describe('flow script path (.js / .mjs / .cjs)'),
|
|
224
|
+
}),
|
|
225
|
+
options: z.object({
|
|
226
|
+
timeout: numberArg.optional().describe('whole-script timeout in ms (default 300000, 0 = unlimited)'),
|
|
227
|
+
}),
|
|
228
|
+
});
|
|
160
229
|
// ── state management ──
|
|
161
230
|
const stateImport = (0, command_1.declareCommand)({
|
|
162
231
|
name: 'state-import',
|
|
@@ -279,6 +348,8 @@ const commandsArray = [
|
|
|
279
348
|
inspect, tree, layout, findings, injectCss,
|
|
280
349
|
// browser
|
|
281
350
|
resize, evalCmd, playwrightCmd, screenshot,
|
|
351
|
+
// interaction
|
|
352
|
+
click, hover, typeCmd, waitForCmd, runCmd,
|
|
282
353
|
// state
|
|
283
354
|
stateImport, stateSave,
|
|
284
355
|
// config
|
package/dist/cli/program.js
CHANGED
|
@@ -70,8 +70,15 @@ async function program() {
|
|
|
70
70
|
case 'eval':
|
|
71
71
|
case 'resize':
|
|
72
72
|
case 'playwright':
|
|
73
|
+
case 'click':
|
|
74
|
+
case 'hover':
|
|
75
|
+
case 'type':
|
|
76
|
+
case 'wait-for':
|
|
73
77
|
await handleSessionCommand(commandName, command, rawArgs, output);
|
|
74
78
|
return;
|
|
79
|
+
case 'run':
|
|
80
|
+
await handleRun(command, rawArgs, output);
|
|
81
|
+
return;
|
|
75
82
|
}
|
|
76
83
|
output.error(`Unknown command: ${commandName}. Run 'cssprobe-cli --help' for usage.`);
|
|
77
84
|
}
|
|
@@ -197,6 +204,16 @@ async function handleStatus(output) {
|
|
|
197
204
|
output.error(e instanceof Error ? e.message : String(e));
|
|
198
205
|
}
|
|
199
206
|
}
|
|
207
|
+
async function handleRun(command, args, output) {
|
|
208
|
+
const fileArg = args._[1];
|
|
209
|
+
if (!fileArg)
|
|
210
|
+
output.error('Usage: cssprobe-cli run <file>');
|
|
211
|
+
const file = path_1.default.resolve(fileArg);
|
|
212
|
+
if (!fs_1.default.existsSync(file))
|
|
213
|
+
output.error(`Flow script not found: ${file}`);
|
|
214
|
+
args._ = ['run', file];
|
|
215
|
+
await handleSessionCommand('run', command, args, output);
|
|
216
|
+
}
|
|
200
217
|
async function handleSessionCommand(commandName, command, args, output) {
|
|
201
218
|
try {
|
|
202
219
|
const cmdArgs = splitArgs(args);
|
|
@@ -214,6 +231,8 @@ async function handleSessionCommand(commandName, command, args, output) {
|
|
|
214
231
|
return;
|
|
215
232
|
}
|
|
216
233
|
console.log(result.text);
|
|
234
|
+
if (typeof result.exitCode === 'number' && result.exitCode !== 0)
|
|
235
|
+
process.exit(result.exitCode);
|
|
217
236
|
}
|
|
218
237
|
catch (e) {
|
|
219
238
|
output.error(e instanceof Error ? e.message : String(e));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Session } from '../daemon/session';
|
|
2
2
|
import type { SessionConfig } from '../daemon/session';
|
|
3
3
|
import type { AnyCommandSchema } from './command';
|
|
4
|
+
export { saveScreenshotFile } from '../utils/screenshot';
|
|
4
5
|
export interface OpenOptions {
|
|
5
6
|
url?: string;
|
|
6
7
|
browser?: string;
|
|
@@ -39,5 +40,5 @@ export declare function getSessionStatus(): Promise<StatusResult>;
|
|
|
39
40
|
export declare function buildDaemonArgs(commandName: string, command: AnyCommandSchema, parsed: Record<string, unknown>): string[];
|
|
40
41
|
export declare function runSessionCommand(commandName: string, command: AnyCommandSchema, parsed: Record<string, unknown>, json?: boolean): Promise<{
|
|
41
42
|
text: string;
|
|
43
|
+
exitCode?: number;
|
|
42
44
|
}>;
|
|
43
|
-
export declare function saveScreenshotFile(dataUrl: string, out?: string): string;
|
package/dist/cli/session-ops.js
CHANGED
|
@@ -1,49 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.saveScreenshotFile = void 0;
|
|
36
4
|
exports.openSession = openSession;
|
|
37
5
|
exports.waitForSession = waitForSession;
|
|
38
6
|
exports.closeSession = closeSession;
|
|
39
7
|
exports.getSessionStatus = getSessionStatus;
|
|
40
8
|
exports.buildDaemonArgs = buildDaemonArgs;
|
|
41
9
|
exports.runSessionCommand = runSessionCommand;
|
|
42
|
-
exports.saveScreenshotFile = saveScreenshotFile;
|
|
43
|
-
const fs = __importStar(require("fs"));
|
|
44
|
-
const os = __importStar(require("os"));
|
|
45
|
-
const path = __importStar(require("path"));
|
|
46
10
|
const session_1 = require("../daemon/session");
|
|
11
|
+
var screenshot_1 = require("../utils/screenshot");
|
|
12
|
+
Object.defineProperty(exports, "saveScreenshotFile", { enumerable: true, get: function () { return screenshot_1.saveScreenshotFile; } });
|
|
47
13
|
async function openSession(opts) {
|
|
48
14
|
const url = opts.url || 'about:blank';
|
|
49
15
|
const browser = opts.browser || 'chromium';
|
|
@@ -142,22 +108,4 @@ async function runSessionCommand(commandName, command, parsed, json = false) {
|
|
|
142
108
|
runArgs.json = true;
|
|
143
109
|
return await session.run(runArgs);
|
|
144
110
|
}
|
|
145
|
-
function saveScreenshotFile(dataUrl, out) {
|
|
146
|
-
const base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
|
|
147
|
-
const buffer = Buffer.from(base64, 'base64');
|
|
148
|
-
const resolvedOut = out
|
|
149
|
-
? path.resolve(out)
|
|
150
|
-
: (() => {
|
|
151
|
-
const dir = path.join(os.homedir(), '.cssprobe-cli', 'screenshots');
|
|
152
|
-
if (!fs.existsSync(dir))
|
|
153
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
154
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
155
|
-
return path.join(dir, `screenshot-${ts}.png`);
|
|
156
|
-
})();
|
|
157
|
-
const dir = path.dirname(resolvedOut);
|
|
158
|
-
if (!fs.existsSync(dir))
|
|
159
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
160
|
-
fs.writeFileSync(resolvedOut, buffer);
|
|
161
|
-
return resolvedOut;
|
|
162
|
-
}
|
|
163
111
|
//# sourceMappingURL=session-ops.js.map
|
package/dist/daemon/daemon.js
CHANGED
|
@@ -17,6 +17,9 @@ const launcher_1 = require("../browser/launcher");
|
|
|
17
17
|
const analyzer_1 = require("../engine/analyzer");
|
|
18
18
|
const renderer_1 = require("../engine/renderer");
|
|
19
19
|
const dom_tree_1 = require("../engine/renderers/dom-tree");
|
|
20
|
+
const interactions_1 = require("../browser/interactions");
|
|
21
|
+
const evalSerializer_1 = require("../engine/evalSerializer");
|
|
22
|
+
const flowRunner_1 = require("./flowRunner");
|
|
20
23
|
const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
21
24
|
async function startDaemon(sessionName, clientInfo, options) {
|
|
22
25
|
const sessionConfig = createSessionConfig(clientInfo, sessionName, options);
|
|
@@ -173,12 +176,18 @@ async function executeCommand(launcher, args) {
|
|
|
173
176
|
const page = launcher.getPage();
|
|
174
177
|
if (!page)
|
|
175
178
|
throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
const handle = await page.evaluateHandle(expression);
|
|
180
|
+
try {
|
|
181
|
+
const result = await handle.evaluate(evalSerializer_1.serializeEvalResult);
|
|
182
|
+
if (typeof result === 'string')
|
|
183
|
+
return { text: result };
|
|
184
|
+
if (result === undefined)
|
|
185
|
+
return { text: 'undefined' };
|
|
186
|
+
return { text: JSON.stringify(result, null, 2) };
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
await handle.dispose();
|
|
190
|
+
}
|
|
182
191
|
}
|
|
183
192
|
case 'resize': {
|
|
184
193
|
const width = parseInt(params[0], 10);
|
|
@@ -206,6 +215,79 @@ async function executeCommand(launcher, args) {
|
|
|
206
215
|
return { text: JSON.stringify({ ok: true, value: null, note: 'call returned no value' }) };
|
|
207
216
|
return { text: JSON.stringify(result, null, 2) };
|
|
208
217
|
}
|
|
218
|
+
case 'click':
|
|
219
|
+
case 'hover': {
|
|
220
|
+
const selector = params[0];
|
|
221
|
+
if (!selector)
|
|
222
|
+
throw new Error(`Selector is required for ${command} command`);
|
|
223
|
+
const page = requirePage(launcher);
|
|
224
|
+
const opts = { nth: args.nth, force: args.force, timeout: args.timeout };
|
|
225
|
+
const result = command === 'click'
|
|
226
|
+
? await (0, interactions_1.clickElement)(page, selector, opts)
|
|
227
|
+
: await (0, interactions_1.hoverElement)(page, selector, opts);
|
|
228
|
+
if (args.json) {
|
|
229
|
+
return { text: JSON.stringify({ ok: true, selector, matches: result.matches, nth: result.nth }, null, 2) };
|
|
230
|
+
}
|
|
231
|
+
return { text: formatTargetAction(command === 'click' ? 'Clicked' : 'Hovered', selector, result) };
|
|
232
|
+
}
|
|
233
|
+
case 'type': {
|
|
234
|
+
const selector = params[0];
|
|
235
|
+
const text = params[1];
|
|
236
|
+
if (!selector || text === undefined)
|
|
237
|
+
throw new Error('Usage: type <selector> <text>');
|
|
238
|
+
await (0, interactions_1.typeInto)(requirePage(launcher), selector, text, {
|
|
239
|
+
press: args.press,
|
|
240
|
+
sequential: args.sequential,
|
|
241
|
+
clear: args.clear,
|
|
242
|
+
delay: args.delay,
|
|
243
|
+
nth: args.nth,
|
|
244
|
+
timeout: args.timeout,
|
|
245
|
+
});
|
|
246
|
+
if (args.json) {
|
|
247
|
+
const payload = { ok: true, selector, length: text.length };
|
|
248
|
+
if (args.press)
|
|
249
|
+
payload.pressed = args.press;
|
|
250
|
+
return { text: JSON.stringify(payload, null, 2) };
|
|
251
|
+
}
|
|
252
|
+
const pressedNote = args.press ? ` then pressed ${args.press}` : '';
|
|
253
|
+
return { text: `Typed ${text.length} chars into ${selector}${pressedNote}` };
|
|
254
|
+
}
|
|
255
|
+
case 'wait-for': {
|
|
256
|
+
// selector is read from the parsed object: option flags are appended to
|
|
257
|
+
// the argv array after positional args, so params[0] is not reliable here.
|
|
258
|
+
const selector = args.selector;
|
|
259
|
+
const timeout = args.timeout ?? 30000;
|
|
260
|
+
const target = (0, interactions_1.resolveWaitTarget)(selector, {
|
|
261
|
+
js: args.js,
|
|
262
|
+
networkIdle: args['network-idle'],
|
|
263
|
+
state: args.state,
|
|
264
|
+
});
|
|
265
|
+
let elapsedMs;
|
|
266
|
+
try {
|
|
267
|
+
elapsedMs = await (0, interactions_1.waitForCondition)(requirePage(launcher), target, timeout);
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
throw new Error(`wait-for failed for ${(0, interactions_1.describeWaitTarget)(target)} within ${timeout}ms: ${e.message}`);
|
|
271
|
+
}
|
|
272
|
+
if (args.json) {
|
|
273
|
+
const payload = {
|
|
274
|
+
ok: true,
|
|
275
|
+
waitedFor: target.kind === 'networkIdle' ? 'networkidle' : target.kind,
|
|
276
|
+
elapsedMs,
|
|
277
|
+
};
|
|
278
|
+
if (target.kind === 'selector')
|
|
279
|
+
payload.state = target.state;
|
|
280
|
+
return { text: JSON.stringify(payload, null, 2) };
|
|
281
|
+
}
|
|
282
|
+
const label = target.kind === 'selector' ? `${selector} (${target.state})` : (0, interactions_1.describeWaitTarget)(target);
|
|
283
|
+
return { text: `Wait satisfied: ${label} (${elapsedMs}ms)` };
|
|
284
|
+
}
|
|
285
|
+
case 'run': {
|
|
286
|
+
const absPath = params[0];
|
|
287
|
+
if (!absPath)
|
|
288
|
+
throw new Error('Flow script path is required');
|
|
289
|
+
return await (0, flowRunner_1.runFlow)(launcher, absPath, { timeout: args.timeout, json: !!args.json });
|
|
290
|
+
}
|
|
209
291
|
case 'state-save': {
|
|
210
292
|
const context = launcher.getContext();
|
|
211
293
|
if (!context)
|
|
@@ -217,6 +299,20 @@ async function executeCommand(launcher, args) {
|
|
|
217
299
|
throw new Error(`Unknown command: ${command}`);
|
|
218
300
|
}
|
|
219
301
|
}
|
|
302
|
+
function requirePage(launcher) {
|
|
303
|
+
const page = launcher.getPage();
|
|
304
|
+
if (!page)
|
|
305
|
+
throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
|
|
306
|
+
return page;
|
|
307
|
+
}
|
|
308
|
+
function formatTargetAction(verb, selector, result) {
|
|
309
|
+
const descriptor = result.descriptor
|
|
310
|
+
? ` → ${result.descriptor.tag}${result.descriptor.classes.map(c => `.${c}`).join('')}${result.descriptor.text ? ` "${result.descriptor.text}"` : ''}`
|
|
311
|
+
: '';
|
|
312
|
+
const matchNote = result.matches === 1 ? '1 match' : `${result.matches} matches`;
|
|
313
|
+
const nthNote = result.matches > 1 ? `, used #${result.nth}` : '';
|
|
314
|
+
return `${verb} ${selector}${descriptor} (${matchNote}${nthNote})`;
|
|
315
|
+
}
|
|
220
316
|
async function socketExists(socketPath) {
|
|
221
317
|
try {
|
|
222
318
|
const stat = await fs_1.default.promises.stat(socketPath);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Page } from 'playwright-core';
|
|
2
|
+
import type { BrowserLauncher } from '../browser/launcher';
|
|
3
|
+
import { type TargetResult, type TypeOptions, type WaitState } from '../browser/interactions';
|
|
4
|
+
import type { Finding, Snapshot } from '../engine/types';
|
|
5
|
+
export interface FlowEnvelope {
|
|
6
|
+
ok: boolean;
|
|
7
|
+
logs: string[];
|
|
8
|
+
result?: unknown;
|
|
9
|
+
error?: string;
|
|
10
|
+
elapsedMs: number;
|
|
11
|
+
}
|
|
12
|
+
export interface WaitForOptions {
|
|
13
|
+
state?: WaitState;
|
|
14
|
+
timeout?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface WaitOutcome {
|
|
17
|
+
waitedFor: 'selector' | 'js' | 'networkidle';
|
|
18
|
+
state?: WaitState;
|
|
19
|
+
elapsedMs: number;
|
|
20
|
+
}
|
|
21
|
+
export interface FlowApi {
|
|
22
|
+
page: Page;
|
|
23
|
+
click(selector: string, opts?: {
|
|
24
|
+
nth?: number;
|
|
25
|
+
force?: boolean;
|
|
26
|
+
timeout?: number;
|
|
27
|
+
}): Promise<TargetResult>;
|
|
28
|
+
hover(selector: string, opts?: {
|
|
29
|
+
nth?: number;
|
|
30
|
+
force?: boolean;
|
|
31
|
+
timeout?: number;
|
|
32
|
+
}): Promise<TargetResult>;
|
|
33
|
+
type(selector: string, text: string, opts?: TypeOptions): Promise<void>;
|
|
34
|
+
waitFor(target: string | {
|
|
35
|
+
js: string;
|
|
36
|
+
timeout?: number;
|
|
37
|
+
} | {
|
|
38
|
+
networkIdle: true;
|
|
39
|
+
timeout?: number;
|
|
40
|
+
}, opts?: WaitForOptions): Promise<WaitOutcome>;
|
|
41
|
+
inspect(selector: string, opts?: {
|
|
42
|
+
format?: 'json' | 'markdown';
|
|
43
|
+
}): Promise<{
|
|
44
|
+
snapshot: Snapshot;
|
|
45
|
+
findings: Finding[];
|
|
46
|
+
} | string>;
|
|
47
|
+
screenshot(opts?: {
|
|
48
|
+
path?: string;
|
|
49
|
+
fullPage?: boolean;
|
|
50
|
+
}): Promise<string>;
|
|
51
|
+
log(...args: unknown[]): void;
|
|
52
|
+
}
|
|
53
|
+
type FlowFn = (api: FlowApi) => Promise<unknown> | unknown;
|
|
54
|
+
export declare function loadFlowScript(absPath: string): Promise<FlowFn>;
|
|
55
|
+
export declare function runFlow(launcher: BrowserLauncher, absPath: string, opts?: {
|
|
56
|
+
timeout?: number;
|
|
57
|
+
json?: boolean;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
text: string;
|
|
60
|
+
exitCode: number;
|
|
61
|
+
}>;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Flow script runner: loads a user script in the daemon process, injects the
|
|
3
|
+
// interaction API, captures console output and returns a result envelope.
|
|
4
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
|
+
};
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.loadFlowScript = loadFlowScript;
|
|
9
|
+
exports.runFlow = runFlow;
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const util_1 = __importDefault(require("util"));
|
|
12
|
+
const url_1 = require("url");
|
|
13
|
+
const interactions_1 = require("../browser/interactions");
|
|
14
|
+
const analyzer_1 = require("../engine/analyzer");
|
|
15
|
+
const renderer_1 = require("../engine/renderer");
|
|
16
|
+
const evalSerializer_1 = require("../engine/evalSerializer");
|
|
17
|
+
const screenshot_1 = require("../utils/screenshot");
|
|
18
|
+
const FLOW_DEFAULT_TIMEOUT = 300_000;
|
|
19
|
+
const CAPTURED_CONSOLE_METHODS = ['log', 'info', 'warn', 'error'];
|
|
20
|
+
// tsc compiles `import()` to `require()` under module=commonjs, which cannot
|
|
21
|
+
// load ESM scripts; a Function-constructed import keeps the native loader.
|
|
22
|
+
const nativeImport = new Function('p', 'return import(p)');
|
|
23
|
+
async function loadFlowScript(absPath) {
|
|
24
|
+
const mod = await nativeImport(`${(0, url_1.pathToFileURL)(absPath).href}?v=${Date.now()}`);
|
|
25
|
+
const fn = mod?.default;
|
|
26
|
+
if (typeof fn !== 'function')
|
|
27
|
+
throw new Error(`Flow script must default-export an async function: ${absPath}`);
|
|
28
|
+
return fn;
|
|
29
|
+
}
|
|
30
|
+
function captureConsole(logs) {
|
|
31
|
+
const originals = new Map();
|
|
32
|
+
for (const method of CAPTURED_CONSOLE_METHODS) {
|
|
33
|
+
originals.set(method, console[method]);
|
|
34
|
+
console[method] = (...args) => {
|
|
35
|
+
logs.push(`[${method}] ${util_1.default.format(...args)}`);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return () => {
|
|
39
|
+
for (const [method, original] of originals) {
|
|
40
|
+
console[method] = original;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function withTimeout(promise, timeoutMs) {
|
|
45
|
+
if (!timeoutMs || timeoutMs <= 0)
|
|
46
|
+
return promise;
|
|
47
|
+
let timer;
|
|
48
|
+
try {
|
|
49
|
+
return await Promise.race([
|
|
50
|
+
promise,
|
|
51
|
+
new Promise((_, reject) => {
|
|
52
|
+
timer = setTimeout(() => reject(new Error(`Flow timed out after ${timeoutMs}ms (best-effort abort)`)), timeoutMs);
|
|
53
|
+
}),
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function requirePage(launcher) {
|
|
61
|
+
const page = launcher.getPage();
|
|
62
|
+
if (!page)
|
|
63
|
+
throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
|
|
64
|
+
return page;
|
|
65
|
+
}
|
|
66
|
+
async function flowWaitFor(page, target, opts) {
|
|
67
|
+
if (typeof target === 'string') {
|
|
68
|
+
const state = opts?.state ?? 'visible';
|
|
69
|
+
const timeout = opts?.timeout ?? 30_000;
|
|
70
|
+
const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'selector', selector: target, state }, timeout);
|
|
71
|
+
return { waitedFor: 'selector', state, elapsedMs };
|
|
72
|
+
}
|
|
73
|
+
if ('js' in target) {
|
|
74
|
+
const timeout = target.timeout ?? 30_000;
|
|
75
|
+
const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'js', expression: target.js }, timeout);
|
|
76
|
+
return { waitedFor: 'js', elapsedMs };
|
|
77
|
+
}
|
|
78
|
+
if (target.networkIdle) {
|
|
79
|
+
const timeout = target.timeout ?? 30_000;
|
|
80
|
+
const elapsedMs = await (0, interactions_1.waitForCondition)(page, { kind: 'networkIdle' }, timeout);
|
|
81
|
+
return { waitedFor: 'networkidle', elapsedMs };
|
|
82
|
+
}
|
|
83
|
+
throw new Error('waitFor requires a selector string, { js }, or { networkIdle: true }');
|
|
84
|
+
}
|
|
85
|
+
function buildFlowApi(launcher, logs, baseDir) {
|
|
86
|
+
const page = requirePage(launcher);
|
|
87
|
+
return {
|
|
88
|
+
page,
|
|
89
|
+
click: (selector, opts) => (0, interactions_1.clickElement)(page, selector, opts),
|
|
90
|
+
hover: (selector, opts) => (0, interactions_1.hoverElement)(page, selector, opts),
|
|
91
|
+
type: (selector, text, opts) => (0, interactions_1.typeInto)(page, selector, text, opts),
|
|
92
|
+
waitFor: (target, opts) => flowWaitFor(page, target, opts),
|
|
93
|
+
inspect: async (selector, opts) => {
|
|
94
|
+
const snapshot = await launcher.collect({
|
|
95
|
+
rootSelector: selector,
|
|
96
|
+
downDepth: 6,
|
|
97
|
+
maxNodes: 60,
|
|
98
|
+
upTo: 'html',
|
|
99
|
+
});
|
|
100
|
+
const findings = (0, analyzer_1.analyze)(snapshot);
|
|
101
|
+
if (opts?.format === 'markdown')
|
|
102
|
+
return (0, renderer_1.renderMarkdown)(snapshot, findings);
|
|
103
|
+
return { snapshot, findings };
|
|
104
|
+
},
|
|
105
|
+
screenshot: async (opts) => {
|
|
106
|
+
const buffer = await page.screenshot({ fullPage: opts?.fullPage });
|
|
107
|
+
const out = opts?.path ? path_1.default.resolve(baseDir, opts.path) : undefined;
|
|
108
|
+
return (0, screenshot_1.saveScreenshotFile)(`data:image/png;base64,${buffer.toString('base64')}`, out);
|
|
109
|
+
},
|
|
110
|
+
log: (...args) => {
|
|
111
|
+
logs.push(util_1.default.format(...args));
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function renderEnvelope(env) {
|
|
116
|
+
const lines = [];
|
|
117
|
+
if (env.ok) {
|
|
118
|
+
lines.push(`Flow completed in ${env.elapsedMs}ms`);
|
|
119
|
+
if (env.result !== undefined)
|
|
120
|
+
lines.push(`Result: ${JSON.stringify(env.result, null, 2)}`);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
lines.push(`Flow failed in ${env.elapsedMs}ms: ${env.error}`);
|
|
124
|
+
}
|
|
125
|
+
if (env.logs.length) {
|
|
126
|
+
lines.push('', 'Logs:');
|
|
127
|
+
lines.push(...env.logs.map(line => ` ${line}`));
|
|
128
|
+
}
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
async function runFlow(launcher, absPath, opts = {}) {
|
|
132
|
+
const logs = [];
|
|
133
|
+
const started = Date.now();
|
|
134
|
+
const restore = captureConsole(logs);
|
|
135
|
+
let env;
|
|
136
|
+
try {
|
|
137
|
+
const fn = await loadFlowScript(absPath);
|
|
138
|
+
const api = buildFlowApi(launcher, logs, path_1.default.dirname(absPath));
|
|
139
|
+
const result = await withTimeout(Promise.resolve(fn(api)), opts.timeout ?? FLOW_DEFAULT_TIMEOUT);
|
|
140
|
+
env = { ok: true, logs, elapsedMs: Date.now() - started };
|
|
141
|
+
if (result !== undefined)
|
|
142
|
+
env.result = (0, evalSerializer_1.serializeEvalResult)(result);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
env = { ok: false, logs, error: e.message, elapsedMs: Date.now() - started };
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
restore();
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
text: opts.json ? JSON.stringify(env, null, 2) : renderEnvelope(env),
|
|
152
|
+
exitCode: env.ok ? 0 : 1,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=flowRunner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function serializeEvalResult(value: unknown): unknown;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Self-contained serializer for eval results.
|
|
3
|
+
// Runs inside the page via JSHandle.evaluate (function source is transferred),
|
|
4
|
+
// and is unit-testable in Node — keep it free of external references.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.serializeEvalResult = serializeEvalResult;
|
|
7
|
+
function serializeEvalResult(value) {
|
|
8
|
+
const seen = new WeakSet();
|
|
9
|
+
const walk = (v, depth) => {
|
|
10
|
+
if (v === null || typeof v !== 'object') {
|
|
11
|
+
if (typeof v === 'function')
|
|
12
|
+
return `[Function: ${v.name || 'anonymous'}]`;
|
|
13
|
+
if (typeof v === 'bigint')
|
|
14
|
+
return `${v}n`;
|
|
15
|
+
return v;
|
|
16
|
+
}
|
|
17
|
+
if (typeof Node !== 'undefined' && v instanceof Node) {
|
|
18
|
+
if (v.nodeType === 1) {
|
|
19
|
+
const el = v;
|
|
20
|
+
return {
|
|
21
|
+
tag: el.tagName.toLowerCase(),
|
|
22
|
+
id: el.id,
|
|
23
|
+
classes: Array.from(el.classList),
|
|
24
|
+
text: (el.textContent ?? '').trim().slice(0, 120),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return `[${v.nodeName}]`;
|
|
28
|
+
}
|
|
29
|
+
if (seen.has(v))
|
|
30
|
+
return '[Circular]';
|
|
31
|
+
if (depth >= 6)
|
|
32
|
+
return '[Depth limit]';
|
|
33
|
+
seen.add(v);
|
|
34
|
+
if (Array.isArray(v))
|
|
35
|
+
return v.map(item => walk(item, depth + 1));
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [key, val] of Object.entries(v))
|
|
38
|
+
out[key] = walk(val, depth + 1);
|
|
39
|
+
return out;
|
|
40
|
+
};
|
|
41
|
+
return walk(value, 0);
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=evalSerializer.js.map
|
package/dist/help.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nSession Management:\n open [url] open browser in session mode (non-blocking)\n close close the browser session\n status show current session status\n resize <width> <height> resize the browser viewport\n eval <expression> evaluate javascript expression on the page (browser context)\n playwright <call> execute playwright api call (node.js context)\n state-import [file] import cookies from netscape format file\n state-save save current browser session state (cookies + localstorage)\n mcp start mcp server (stdio transport) for ai clients\n mcp-install install mcp server config into ai client configs\n mcp-uninstall remove mcp server config from ai client configs\n\nCSS Inspection:\n inspect <selector> inspect the runtime css of a page element\n tree <selector> show dom tree structure\n layout <selector> show ascii layout diagram\n findings <selector> show only issues/warnings/errors\n inject-css <css> inject css into the current page\n\nExport:\n screenshot take a screenshot of the current page\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
|
|
2
|
+
"global": "Usage: cssprobe-cli <command> [args] [options]\n\n\nSession Management:\n open [url] open browser in session mode (non-blocking)\n close close the browser session\n status show current session status\n resize <width> <height> resize the browser viewport\n eval <expression> evaluate javascript expression on the page (browser context)\n playwright <call> execute playwright api call (node.js context)\n click <selector> click an element with real mouse events (works with custom react components)\n hover <selector> hover an element with real mouse events\n type <selector> <text> type text into an element (fill by default; --sequential for real key events)\n wait-for [selector] wait for a condition: selector state, js predicate, or network idle\n run <file> run a multi-step flow script (default-exports an async function receiving the api object)\n state-import [file] import cookies from netscape format file\n state-save save current browser session state (cookies + localstorage)\n mcp start mcp server (stdio transport) for ai clients\n mcp-install install mcp server config into ai client configs\n mcp-uninstall remove mcp server config from ai client configs\n\nCSS Inspection:\n inspect <selector> inspect the runtime css of a page element\n tree <selector> show dom tree structure\n layout <selector> show ascii layout diagram\n findings <selector> show only issues/warnings/errors\n inject-css <css> inject css into the current page\n\nExport:\n screenshot take a screenshot of the current page\n\nConfiguration:\n config-show show current configuration\n config-set <key> <value> set a configuration value\n config-list list all profiles\n config-use <name> switch active profile\n config-new <name> create a new profile\n config-path show configuration file path\n\nAgent Skills:\n skill-install install agent skill for cssprobe-cli\n skill-uninstall remove agent skill for cssprobe-cli\n\nGlobal options:\n --help [command] print help\n --json output response as JSON\n --raw output only the result value\n --version print version\n -p, --profile <name> use named profile (or CSSPROBE_PROFILE env)",
|
|
3
3
|
"commands": {
|
|
4
4
|
"open": {
|
|
5
5
|
"help": "cssprobe-cli open [url]\n\nOpen browser in session mode (non-blocking)\n\nArguments:\n [url] url, file:// path, or local html file path\nOptions:\n --browser browser engine (default chromium)\n --headed show the browser window\n --viewport viewport size as wxh (e.g. 1280x720)\n --state path to saved state file (cookies + localstorage)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
@@ -97,6 +97,64 @@
|
|
|
97
97
|
},
|
|
98
98
|
"args": []
|
|
99
99
|
},
|
|
100
|
+
"click": {
|
|
101
|
+
"help": "cssprobe-cli click <selector>\n\nClick an element with real mouse events (works with custom React components)\n\nArguments:\n <selector> css selector\nOptions:\n --nth 1-based match index (default: first match)\n --force skip actionability checks\n --timeout action timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
102
|
+
"flags": {
|
|
103
|
+
"nth": "string",
|
|
104
|
+
"force": "boolean",
|
|
105
|
+
"timeout": "string"
|
|
106
|
+
},
|
|
107
|
+
"args": [
|
|
108
|
+
"selector"
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"hover": {
|
|
112
|
+
"help": "cssprobe-cli hover <selector>\n\nHover an element with real mouse events\n\nArguments:\n <selector> css selector\nOptions:\n --nth 1-based match index (default: first match)\n --force skip actionability checks\n --timeout action timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
113
|
+
"flags": {
|
|
114
|
+
"nth": "string",
|
|
115
|
+
"force": "boolean",
|
|
116
|
+
"timeout": "string"
|
|
117
|
+
},
|
|
118
|
+
"args": [
|
|
119
|
+
"selector"
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
"type": {
|
|
123
|
+
"help": "cssprobe-cli type <selector> <text>\n\nType text into an element (fill by default; --sequential for real key events)\n\nArguments:\n <selector> css selector\n <text> text to type\nOptions:\n --press key to press after typing (e.g. enter, tab, control+a)\n --sequential type with real key events (for custom editors)\n --clear clear existing content before sequential typing\n --delay delay between keystrokes in ms (--sequential, default 0)\n --nth 1-based match index (default: first match)\n --timeout action timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
124
|
+
"flags": {
|
|
125
|
+
"press": "string",
|
|
126
|
+
"sequential": "boolean",
|
|
127
|
+
"clear": "boolean",
|
|
128
|
+
"delay": "string",
|
|
129
|
+
"nth": "string",
|
|
130
|
+
"timeout": "string"
|
|
131
|
+
},
|
|
132
|
+
"args": [
|
|
133
|
+
"selector",
|
|
134
|
+
"text"
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
"wait-for": {
|
|
138
|
+
"help": "cssprobe-cli wait-for [selector]\n\nWait for a condition: selector state, JS predicate, or network idle\n\nArguments:\n [selector] css selector to wait for\nOptions:\n --state selector state to wait for (default visible)\n --js js predicate, e.g. \"() => document.queryselector('.x')?.offsetheight > 100\"\n --network-idle wait until the page is network-idle\n --timeout timeout in ms (default 30000)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
139
|
+
"flags": {
|
|
140
|
+
"state": "string",
|
|
141
|
+
"js": "string",
|
|
142
|
+
"network-idle": "boolean",
|
|
143
|
+
"timeout": "string"
|
|
144
|
+
},
|
|
145
|
+
"args": [
|
|
146
|
+
"selector"
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
"run": {
|
|
150
|
+
"help": "cssprobe-cli run <file>\n\nRun a multi-step flow script (default-exports an async function receiving the API object)\n\nArguments:\n <file> flow script path (.js / .mjs / .cjs)\nOptions:\n --timeout whole-script timeout in ms (default 300000, 0 = unlimited)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
151
|
+
"flags": {
|
|
152
|
+
"timeout": "string"
|
|
153
|
+
},
|
|
154
|
+
"args": [
|
|
155
|
+
"file"
|
|
156
|
+
]
|
|
157
|
+
},
|
|
100
158
|
"state-import": {
|
|
101
159
|
"help": "cssprobe-cli state-import [file]\n\nImport cookies from Netscape format file\n\nArguments:\n [file] netscape cookie file path (reads from stdin if omitted)\nOptions:\n --out output state file path (default: ~/.cssprobe-cli/states/imported.json)\n --name custom name for the state file (e.g. mysite or mysite.json)\n --merge existing state file to merge into\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
|
|
102
160
|
"flags": {
|
|
@@ -203,6 +261,10 @@
|
|
|
203
261
|
"brief",
|
|
204
262
|
"layout",
|
|
205
263
|
"full-page",
|
|
264
|
+
"force",
|
|
265
|
+
"sequential",
|
|
266
|
+
"clear",
|
|
267
|
+
"network-idle",
|
|
206
268
|
"local"
|
|
207
269
|
]
|
|
208
270
|
}
|
|
@@ -127,6 +127,49 @@ function buildSkillMd() {
|
|
|
127
127
|
'cssprobe-cli close # stop browser + daemon',
|
|
128
128
|
BT3,
|
|
129
129
|
'',
|
|
130
|
+
'## Multi-step UI Flows',
|
|
131
|
+
'',
|
|
132
|
+
'Beyond inspection, cssprobe-cli drives the page: click, hover, type, wait, and',
|
|
133
|
+
'run scripted flows -- all in the same session.',
|
|
134
|
+
'',
|
|
135
|
+
BT3 + 'bash',
|
|
136
|
+
'cssprobe-cli click ".layout-switch" # real mouse click (auto-scroll + actionability wait)',
|
|
137
|
+
'cssprobe-cli click ".item" --nth 2 # 1-based match index; multi-match without --nth fails (strict)',
|
|
138
|
+
'cssprobe-cli hover ".menu-item"',
|
|
139
|
+
'cssprobe-cli type ".block-title input" "Hi" --press Enter # fill by default',
|
|
140
|
+
'cssprobe-cli type ".editor" "text" --sequential --clear # real key events for custom editors',
|
|
141
|
+
'cssprobe-cli wait-for ".modal" --state visible --timeout 5000',
|
|
142
|
+
'cssprobe-cli wait-for --js "() => ready === true"',
|
|
143
|
+
'cssprobe-cli wait-for --network-idle',
|
|
144
|
+
BT3,
|
|
145
|
+
'',
|
|
146
|
+
'### Scripted flows: ' + BT + 'run <file>' + BT,
|
|
147
|
+
'',
|
|
148
|
+
'A flow script default-exports an async function receiving the API object and',
|
|
149
|
+
'runs inside the daemon session:',
|
|
150
|
+
'',
|
|
151
|
+
BT3 + 'js',
|
|
152
|
+
'export default async ({ page, click, hover, type, waitFor, inspect, screenshot, log }) => {',
|
|
153
|
+
' await waitFor(".editor-canvas");',
|
|
154
|
+
' await click(".layout-switch", { nth: 1 });',
|
|
155
|
+
' await type(".block-title input", "Hello", { press: "Enter", sequential: true, clear: true });',
|
|
156
|
+
' await waitFor({ networkIdle: true });',
|
|
157
|
+
' const { findings } = await inspect(".block-grid", { format: "json" });',
|
|
158
|
+
' await screenshot({ path: "./after.png" });',
|
|
159
|
+
' return { findings: findings.length };',
|
|
160
|
+
'};',
|
|
161
|
+
BT3,
|
|
162
|
+
'',
|
|
163
|
+
BT3 + 'bash',
|
|
164
|
+
'cssprobe-cli run ./flow.js # --timeout ms (default 300000, 0 = unlimited)',
|
|
165
|
+
BT3,
|
|
166
|
+
'',
|
|
167
|
+
'Notes:',
|
|
168
|
+
'',
|
|
169
|
+
'- Scripts run on your machine with full Node privileges -- only run trusted scripts.',
|
|
170
|
+
'- ' + BT + 'screenshot' + BT + ' relative paths resolve against the script directory.',
|
|
171
|
+
'- On failure the command exits 1, prints collected logs, and keeps the session alive.',
|
|
172
|
+
'',
|
|
130
173
|
'## Output Modes',
|
|
131
174
|
'',
|
|
132
175
|
'Pass ' + BT + '--json' + BT + ' when consuming output programmatically:',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function saveScreenshotFile(dataUrl: string, out?: string): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.saveScreenshotFile = saveScreenshotFile;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
function saveScreenshotFile(dataUrl, out) {
|
|
41
|
+
const base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
|
|
42
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
43
|
+
const resolvedOut = out
|
|
44
|
+
? path.resolve(out)
|
|
45
|
+
: (() => {
|
|
46
|
+
const dir = path.join(os.homedir(), '.cssprobe-cli', 'screenshots');
|
|
47
|
+
if (!fs.existsSync(dir))
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
50
|
+
return path.join(dir, `screenshot-${ts}.png`);
|
|
51
|
+
})();
|
|
52
|
+
const dir = path.dirname(resolvedOut);
|
|
53
|
+
if (!fs.existsSync(dir))
|
|
54
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
55
|
+
fs.writeFileSync(resolvedOut, buffer);
|
|
56
|
+
return resolvedOut;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=screenshot.js.map
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cssprobe-cli
|
|
2
2
|
|
|
3
|
-
> Runtime CSS probe — inspect layout, scroll, colors, backgrounds, fonts in a live browser. Returns DOM tree, ASCII layout diagram and confidence-classified findings for humans and AI agents.
|
|
3
|
+
> Runtime CSS probe — inspect layout, scroll, colors, backgrounds, fonts in a live browser. Returns DOM tree, ASCII layout diagram and confidence-classified findings for humans and AI agents. Interaction commands (click / hover / type / wait-for) drive the page; `run <file>` executes scripted multi-step flows.
|
|
4
4
|
|
|
5
5
|
## Documentation
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cssprobe-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"mcpName": "io.github.mack-peng/cssprobe-cli",
|
|
5
5
|
"description": "Runtime CSS probe — inspect layout, colors, backgrounds, fonts and more in a live browser. AI agents and shell automation.",
|
|
6
6
|
"keywords": [
|