cssprobe-cli 0.3.1 → 0.4.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 (40) hide show
  1. package/AGENTS.md +35 -7
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +54 -4
  4. package/dist/browser/frames.d.ts +15 -0
  5. package/dist/browser/frames.js +60 -0
  6. package/dist/browser/interactions.d.ts +1 -0
  7. package/dist/browser/interactions.js +9 -1
  8. package/dist/browser/launcher.d.ts +30 -2
  9. package/dist/browser/launcher.js +141 -34
  10. package/dist/browser/sampler.d.ts +43 -0
  11. package/dist/browser/sampler.js +111 -0
  12. package/dist/cli/commands.d.ts +3 -0
  13. package/dist/cli/commands.js +92 -5
  14. package/dist/cli/input.d.ts +6 -0
  15. package/dist/cli/input.js +24 -0
  16. package/dist/cli/program.js +152 -1
  17. package/dist/cli/session-ops.d.ts +8 -0
  18. package/dist/cli/session-ops.js +24 -1
  19. package/dist/cli/state-check.d.ts +17 -0
  20. package/dist/cli/state-check.js +43 -0
  21. package/dist/cli/url-guard.d.ts +2 -0
  22. package/dist/cli/url-guard.js +24 -0
  23. package/dist/daemon/daemon.js +67 -19
  24. package/dist/daemon/expect.d.ts +8 -0
  25. package/dist/daemon/expect.js +47 -0
  26. package/dist/daemon/flowRunner.d.ts +12 -1
  27. package/dist/daemon/flowRunner.js +22 -0
  28. package/dist/engine/evalSerializer.js +2 -2
  29. package/dist/help.json +94 -16
  30. package/dist/scenario/assertions.d.ts +13 -0
  31. package/dist/scenario/assertions.js +111 -0
  32. package/dist/scenario/reporters.d.ts +13 -0
  33. package/dist/scenario/reporters.js +79 -0
  34. package/dist/scenario/runner.d.ts +10 -0
  35. package/dist/scenario/runner.js +244 -0
  36. package/dist/scenario/schema.d.ts +1210 -0
  37. package/dist/scenario/schema.js +145 -0
  38. package/docs/guide/installation.md +1 -1
  39. package/llms.txt +1 -1
  40. package/package.json +1 -1
@@ -0,0 +1,17 @@
1
+ export interface StorageCookie {
2
+ name: string;
3
+ domain: string;
4
+ expires?: number;
5
+ }
6
+ export interface StateReport {
7
+ cookies: number;
8
+ domains: string[];
9
+ expired: string[];
10
+ sessionCookies: number;
11
+ targetMatches: boolean | null;
12
+ warnings: string[];
13
+ ok: boolean;
14
+ }
15
+ export declare function analyzeState(state: {
16
+ cookies?: StorageCookie[];
17
+ }, targetUrl?: string, nowMs?: number): StateReport;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeState = analyzeState;
4
+ function normalizeDomain(domain) {
5
+ return domain.replace(/^\./, '').toLowerCase();
6
+ }
7
+ function analyzeState(state, targetUrl, nowMs = Date.now()) {
8
+ const cookies = state.cookies ?? [];
9
+ const expired = [];
10
+ let sessionCookies = 0;
11
+ for (const cookie of cookies) {
12
+ if (cookie.expires === undefined || cookie.expires <= 0) {
13
+ sessionCookies++;
14
+ }
15
+ else if (cookie.expires * 1000 <= nowMs) {
16
+ expired.push(cookie.name);
17
+ }
18
+ }
19
+ const domains = Array.from(new Set(cookies.map(cookie => normalizeDomain(cookie.domain)))).sort();
20
+ let targetMatches = null;
21
+ if (targetUrl) {
22
+ try {
23
+ const host = new URL(targetUrl).hostname.toLowerCase();
24
+ targetMatches = cookies.some(cookie => {
25
+ const domain = normalizeDomain(cookie.domain);
26
+ return host === domain || host.endsWith(`.${domain}`);
27
+ });
28
+ }
29
+ catch {
30
+ throw new Error(`Invalid --url value: ${targetUrl}`);
31
+ }
32
+ }
33
+ const warnings = [];
34
+ if (!cookies.length)
35
+ warnings.push('no cookies in state file');
36
+ if (expired.length)
37
+ warnings.push(`expired cookies: ${expired.join(', ')}`);
38
+ if (targetMatches === false)
39
+ warnings.push(`no cookies match ${targetUrl}`);
40
+ const ok = cookies.length > 0 && expired.length === 0 && targetMatches !== false;
41
+ return { cookies: cookies.length, domains, expired, sessionCookies, targetMatches, warnings, ok };
42
+ }
43
+ //# sourceMappingURL=state-check.js.map
@@ -0,0 +1,2 @@
1
+ export declare function looksLikeLoginUrl(raw: string): boolean;
2
+ export declare function matchesUrlPattern(pattern: string, raw: string): boolean;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.looksLikeLoginUrl = looksLikeLoginUrl;
4
+ exports.matchesUrlPattern = matchesUrlPattern;
5
+ function looksLikeLoginUrl(raw) {
6
+ try {
7
+ const url = new URL(raw);
8
+ return /\/(?:s\/)?login(?:\/|$)/.test(url.pathname);
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function matchesUrlPattern(pattern, raw) {
15
+ let regex;
16
+ try {
17
+ regex = new RegExp(pattern);
18
+ }
19
+ catch {
20
+ throw new Error(`Invalid --expect-url pattern: ${pattern}`);
21
+ }
22
+ return regex.test(raw);
23
+ }
24
+ //# sourceMappingURL=url-guard.js.map
@@ -20,6 +20,8 @@ const dom_tree_1 = require("../engine/renderers/dom-tree");
20
20
  const interactions_1 = require("../browser/interactions");
21
21
  const evalSerializer_1 = require("../engine/evalSerializer");
22
22
  const flowRunner_1 = require("./flowRunner");
23
+ const frames_1 = require("../browser/frames");
24
+ const sampler_1 = require("../browser/sampler");
23
25
  const AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
24
26
  async function startDaemon(sessionName, clientInfo, options) {
25
27
  const sessionConfig = createSessionConfig(clientInfo, sessionName, options);
@@ -77,6 +79,18 @@ async function executeCommand(launcher, args) {
77
79
  const command = args._[0];
78
80
  const params = args._.slice(1);
79
81
  switch (command) {
82
+ case 'frames': {
83
+ const frames = (0, frames_1.describeFrames)(requirePage(launcher));
84
+ if (args.json)
85
+ return { text: JSON.stringify(frames, null, 2) };
86
+ return { text: (0, frames_1.formatFrames)(frames) };
87
+ }
88
+ case 'page-info': {
89
+ const page = requirePage(launcher);
90
+ return {
91
+ text: JSON.stringify({ url: page.url(), title: await page.title(), frames: (0, frames_1.describeFrames)(page).length }, null, 2),
92
+ };
93
+ }
80
94
  case 'goto': {
81
95
  const url = params[0] || 'about:blank';
82
96
  await launcher.open(url);
@@ -91,7 +105,7 @@ async function executeCommand(launcher, args) {
91
105
  downDepth: args.depth || 6,
92
106
  maxNodes: 60,
93
107
  upTo: 'html',
94
- });
108
+ }, (0, frames_1.resolveFrame)(requirePage(launcher), args.frame));
95
109
  const findings = (0, analyzer_1.analyze)(snapshot);
96
110
  if (args.json) {
97
111
  return { text: JSON.stringify((0, renderer_1.renderJSON)(snapshot, findings, args.brief), null, 2) };
@@ -109,7 +123,7 @@ async function executeCommand(launcher, args) {
109
123
  downDepth: args.depth || 6,
110
124
  maxNodes: 60,
111
125
  upTo: 'html',
112
- });
126
+ }, (0, frames_1.resolveFrame)(requirePage(launcher), args.frame));
113
127
  // Render tree only
114
128
  const lines = [];
115
129
  lines.push(`## DOM tree (${snapshot.downDepth} levels deep)`);
@@ -127,7 +141,7 @@ async function executeCommand(launcher, args) {
127
141
  downDepth: 6,
128
142
  maxNodes: 60,
129
143
  upTo: 'html',
130
- });
144
+ }, (0, frames_1.resolveFrame)(requirePage(launcher), args.frame));
131
145
  const findings = (0, analyzer_1.analyze)(snapshot);
132
146
  return { text: (0, renderer_1.renderMarkdown)(snapshot, findings, false, true) };
133
147
  }
@@ -140,7 +154,7 @@ async function executeCommand(launcher, args) {
140
154
  downDepth: 6,
141
155
  maxNodes: 60,
142
156
  upTo: 'html',
143
- });
157
+ }, (0, frames_1.resolveFrame)(requirePage(launcher), args.frame));
144
158
  const findings = (0, analyzer_1.analyze)(snapshot);
145
159
  const important = findings.filter(f => f.level === 'warning' || f.level === 'error');
146
160
  if (important.length === 0) {
@@ -155,11 +169,9 @@ async function executeCommand(launcher, args) {
155
169
  const css = params[0];
156
170
  if (!css)
157
171
  throw new Error('CSS is required for inject-css command');
158
- const page = launcher.getPage();
159
- if (!page)
160
- throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
161
- await page.addStyleTag({ content: css });
162
- return { text: JSON.stringify({ success: true }) };
172
+ const frame = (0, frames_1.resolveFrame)(requirePage(launcher), args.frame);
173
+ await frame.addStyleTag({ content: css });
174
+ return { text: JSON.stringify({ success: true, frame: frame.name() || frame.url() }) };
163
175
  }
164
176
  case 'screenshot': {
165
177
  const page = launcher.getPage();
@@ -173,17 +185,21 @@ async function executeCommand(launcher, args) {
173
185
  const expression = params[0];
174
186
  if (!expression)
175
187
  throw new Error('Expression is required for eval command');
176
- const page = launcher.getPage();
177
- if (!page)
178
- throw new Error('Browser is not open. Run: cssprobe-cli open <url>');
179
- const handle = await page.evaluateHandle(expression);
188
+ const frame = (0, frames_1.resolveFrame)(requirePage(launcher), args.frame);
189
+ const handle = await frame.evaluateHandle(expression);
180
190
  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) };
191
+ const resolved = await handle.evaluateHandle((value) => Promise.resolve(value));
192
+ try {
193
+ const result = await resolved.evaluate(evalSerializer_1.serializeEvalResult);
194
+ if (typeof result === 'string')
195
+ return { text: result };
196
+ if (result === undefined)
197
+ return { text: 'undefined' };
198
+ return { text: JSON.stringify(result, null, 2) };
199
+ }
200
+ finally {
201
+ await resolved.dispose();
202
+ }
187
203
  }
188
204
  finally {
189
205
  await handle.dispose();
@@ -282,6 +298,38 @@ async function executeCommand(launcher, args) {
282
298
  const label = target.kind === 'selector' ? `${selector} (${target.state})` : (0, interactions_1.describeWaitTarget)(target);
283
299
  return { text: `Wait satisfied: ${label} (${elapsedMs}ms)` };
284
300
  }
301
+ case 'sample': {
302
+ const expression = params[0];
303
+ if (!expression)
304
+ throw new Error('Expression is required for sample command');
305
+ const interval = args.interval ?? 250;
306
+ const duration = args.duration ?? 5000;
307
+ if (duration <= 0)
308
+ throw new Error('--duration must be greater than 0');
309
+ if (interval <= 0)
310
+ throw new Error('--interval must be greater than 0');
311
+ const frame = (0, frames_1.resolveFrame)(requirePage(launcher), args.frame);
312
+ const points = await (0, sampler_1.sampleFrame)(frame, expression, {
313
+ intervalMs: interval,
314
+ durationMs: duration,
315
+ ignoreErrors: !!args['ignore-errors'],
316
+ });
317
+ const summary = (0, sampler_1.summarizeSamples)(points, args.key, 5000, args['tail-from']);
318
+ return { text: JSON.stringify({ expression, intervalMs: interval, summary, series: points }, null, 2) };
319
+ }
320
+ case 'wait-stable': {
321
+ const expression = params[0];
322
+ if (!expression)
323
+ throw new Error('Expression is required for wait-stable command');
324
+ const frame = (0, frames_1.resolveFrame)(requirePage(launcher), args.frame);
325
+ const result = await (0, sampler_1.waitForStableValue)(frame, expression, {
326
+ tolerance: args.tolerance,
327
+ windowMs: args.window,
328
+ timeoutMs: args.timeout,
329
+ intervalMs: args.interval,
330
+ });
331
+ return { text: JSON.stringify({ ok: true, expression, ...result }, null, 2) };
332
+ }
285
333
  case 'run': {
286
334
  const absPath = params[0];
287
335
  if (!absPath)
@@ -0,0 +1,8 @@
1
+ export interface Matchers {
2
+ toBe(expected: unknown, label?: string): void;
3
+ toBeWithin(expected: number, tolerance: number, label?: string): void;
4
+ toMatch(pattern: RegExp | string, label?: string): void;
5
+ toHaveHash(hash: string, label?: string): void;
6
+ toBeSelected(index: number, label?: string): void;
7
+ }
8
+ export declare function expect(actual: unknown): Matchers;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.expect = expect;
4
+ function labelSuffix(label) {
5
+ return label ? ` (${label})` : '';
6
+ }
7
+ function fail(label, message) {
8
+ throw new Error(`expect${labelSuffix(label)}: ${message}`);
9
+ }
10
+ function expect(actual) {
11
+ return {
12
+ toBe(expected, label) {
13
+ if (actual !== expected) {
14
+ fail(label, `expected ${JSON.stringify(expected)}, received ${JSON.stringify(actual)}`);
15
+ }
16
+ },
17
+ toBeWithin(expected, tolerance, label) {
18
+ if (typeof actual !== 'number' || !Number.isFinite(actual)) {
19
+ fail(label, `expected a finite number near ${expected}, received ${JSON.stringify(actual)}`);
20
+ }
21
+ if (Math.abs(actual - expected) > tolerance) {
22
+ fail(label, `expected ${expected} ±${tolerance}, received ${actual}`);
23
+ }
24
+ },
25
+ toMatch(pattern, label) {
26
+ if (typeof actual !== 'string') {
27
+ fail(label, `expected a string, received ${JSON.stringify(actual)}`);
28
+ }
29
+ const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
30
+ if (!regex.test(actual)) {
31
+ fail(label, `expected ${JSON.stringify(actual)} to match ${regex}`);
32
+ }
33
+ },
34
+ toHaveHash(hash, label) {
35
+ const expected = hash.startsWith('#') ? hash : `#${hash}`;
36
+ if (actual !== expected) {
37
+ fail(label, `expected hash ${expected}, received ${JSON.stringify(actual)}`);
38
+ }
39
+ },
40
+ toBeSelected(index, label) {
41
+ if (actual !== index) {
42
+ fail(label, `expected selected index ${index}, received ${JSON.stringify(actual)}`);
43
+ }
44
+ },
45
+ };
46
+ }
47
+ //# sourceMappingURL=expect.js.map
@@ -1,13 +1,20 @@
1
- import type { Page } from 'playwright-core';
1
+ import type { BrowserContext, Page } from 'playwright-core';
2
2
  import type { BrowserLauncher } from '../browser/launcher';
3
3
  import { type TargetResult, type TypeOptions, type WaitState } from '../browser/interactions';
4
4
  import type { Finding, Snapshot } from '../engine/types';
5
+ import { type FrameInfo } from '../browser/frames';
6
+ import { type Matchers } from './expect';
7
+ export interface FlowPageError {
8
+ message: string;
9
+ stack?: string;
10
+ }
5
11
  export interface FlowEnvelope {
6
12
  ok: boolean;
7
13
  logs: string[];
8
14
  result?: unknown;
9
15
  error?: string;
10
16
  elapsedMs: number;
17
+ pageErrors?: FlowPageError[];
11
18
  }
12
19
  export interface WaitForOptions {
13
20
  state?: WaitState;
@@ -20,6 +27,10 @@ export interface WaitOutcome {
20
27
  }
21
28
  export interface FlowApi {
22
29
  page: Page;
30
+ context: BrowserContext;
31
+ frames(): FrameInfo[];
32
+ newPage(): Promise<Page>;
33
+ expect(actual: unknown): Matchers;
23
34
  click(selector: string, opts?: {
24
35
  nth?: number;
25
36
  force?: boolean;
@@ -15,6 +15,8 @@ const analyzer_1 = require("../engine/analyzer");
15
15
  const renderer_1 = require("../engine/renderer");
16
16
  const evalSerializer_1 = require("../engine/evalSerializer");
17
17
  const screenshot_1 = require("../utils/screenshot");
18
+ const frames_1 = require("../browser/frames");
19
+ const expect_1 = require("./expect");
18
20
  const FLOW_DEFAULT_TIMEOUT = 300_000;
19
21
  const CAPTURED_CONSOLE_METHODS = ['log', 'info', 'warn', 'error'];
20
22
  // tsc compiles `import()` to `require()` under module=commonjs, which cannot
@@ -102,6 +104,10 @@ function buildFlowApi(launcher, logs, baseDir) {
102
104
  return (0, renderer_1.renderMarkdown)(snapshot, findings);
103
105
  return { snapshot, findings };
104
106
  },
107
+ context: page.context(),
108
+ frames: () => (0, frames_1.describeFrames)(page),
109
+ newPage: () => page.context().newPage(),
110
+ expect: (actual) => (0, expect_1.expect)(actual),
105
111
  screenshot: async (opts) => {
106
112
  const buffer = await page.screenshot({ fullPage: opts?.fullPage });
107
113
  const out = opts?.path ? path_1.default.resolve(baseDir, opts.path) : undefined;
@@ -122,6 +128,10 @@ function renderEnvelope(env) {
122
128
  else {
123
129
  lines.push(`Flow failed in ${env.elapsedMs}ms: ${env.error}`);
124
130
  }
131
+ if (env.pageErrors?.length) {
132
+ lines.push('', `Page errors (${env.pageErrors.length}):`);
133
+ lines.push(...env.pageErrors.map(error => ` - ${error.message}`));
134
+ }
125
135
  if (env.logs.length) {
126
136
  lines.push('', 'Logs:');
127
137
  lines.push(...env.logs.map(line => ` ${line}`));
@@ -132,6 +142,15 @@ async function runFlow(launcher, absPath, opts = {}) {
132
142
  const logs = [];
133
143
  const started = Date.now();
134
144
  const restore = captureConsole(logs);
145
+ const pageErrors = [];
146
+ const page = launcher.getPage();
147
+ const onPageError = (error) => {
148
+ pageErrors.push({
149
+ message: error.message,
150
+ stack: error.stack?.split('\n').slice(0, 6).join('\n'),
151
+ });
152
+ };
153
+ page?.on('pageerror', onPageError);
135
154
  let env;
136
155
  try {
137
156
  const fn = await loadFlowScript(absPath);
@@ -145,8 +164,11 @@ async function runFlow(launcher, absPath, opts = {}) {
145
164
  env = { ok: false, logs, error: e.message, elapsedMs: Date.now() - started };
146
165
  }
147
166
  finally {
167
+ page?.off('pageerror', onPageError);
148
168
  restore();
149
169
  }
170
+ if (pageErrors.length)
171
+ env.pageErrors = pageErrors;
150
172
  return {
151
173
  text: opts.json ? JSON.stringify(env, null, 2) : renderEnvelope(env),
152
174
  exitCode: env.ok ? 0 : 1,
@@ -32,10 +32,10 @@ function serializeEvalResult(value) {
32
32
  return '[Depth limit]';
33
33
  seen.add(v);
34
34
  if (Array.isArray(v))
35
- return v.map(item => walk(item, depth + 1));
35
+ return Array.from(v, item => (item === undefined ? null : walk(item, depth + 1)));
36
36
  const out = {};
37
37
  for (const [key, val] of Object.entries(v))
38
- out[key] = walk(val, depth + 1);
38
+ out[key] = val === undefined ? null : walk(val, depth + 1);
39
39
  return out;
40
40
  };
41
41
  return walk(value, 0);
package/dist/help.json CHANGED
@@ -1,13 +1,15 @@
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 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)",
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 frames list frames in the current page\n doctor diagnose browser installation and playwright revision mismatch\n scenario <action> <file> run a json scenario suite (regression cases, artifacts, junit)\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 sample <expression> sample a js expression over time and return a timeline with summary stats\n wait-stable <expression> wait until a js expression stops changing (tolerance + stable window)\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 state-check validate a saved state file: cookies, expiry, and target domain coverage\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
- "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",
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 --expect-url regex the final page url must match\n --fail-on-login fail when the page redirects to a login url\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
6
6
  "flags": {
7
7
  "browser": "string",
8
8
  "headed": "boolean",
9
9
  "viewport": "string",
10
- "state": "string"
10
+ "state": "string",
11
+ "expect-url": "string",
12
+ "fail-on-login": "boolean"
11
13
  },
12
14
  "args": [
13
15
  "url"
@@ -25,44 +27,80 @@
25
27
  "flags": {},
26
28
  "args": []
27
29
  },
30
+ "frames": {
31
+ "help": "cssprobe-cli frames \n\nList frames in the current page\n\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
32
+ "flags": {},
33
+ "args": []
34
+ },
35
+ "doctor": {
36
+ "help": "cssprobe-cli doctor \n\nDiagnose browser installation and Playwright revision mismatch\n\nOptions:\n --engine browser engine: chromium, firefox, webkit (default: chromium)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
37
+ "flags": {
38
+ "engine": "string"
39
+ },
40
+ "args": []
41
+ },
42
+ "scenario": {
43
+ "help": "cssprobe-cli scenario <action> <file>\n\nRun a JSON scenario suite (regression cases, artifacts, JUnit)\n\nArguments:\n <action> only \"run\" is supported\n <file> scenario suite json path\nOptions:\n --state fallback state file when the suite/case does not set one\n --artifacts artifact directory (default: ~/.cssprobe-cli/scenario-runs/<suite>-<timestamp>)\n --junit write a junit xml report to this path\n --bail stop after the first failing case\n --headed show the browser window\n --browser browser engine (default chromium)\n --viewport viewport size as wxh (default: suite defaults or 1280x720)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
44
+ "flags": {
45
+ "state": "string",
46
+ "artifacts": "string",
47
+ "junit": "string",
48
+ "bail": "boolean",
49
+ "headed": "boolean",
50
+ "browser": "string",
51
+ "viewport": "string"
52
+ },
53
+ "args": [
54
+ "action",
55
+ "file"
56
+ ]
57
+ },
28
58
  "inspect": {
29
- "help": "cssprobe-cli inspect <selector>\n\nInspect the runtime CSS of a page element\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --json output structured json instead of markdown\n --brief compact output: tree sketch + warnings/errors only\n --layout ascii layout diagram showing element positions and sizes\n --depth dom tree depth (default: auto, max 20)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
59
+ "help": "cssprobe-cli inspect <selector>\n\nInspect the runtime CSS of a page element\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --json output structured json instead of markdown\n --brief compact output: tree sketch + warnings/errors only\n --layout ascii layout diagram showing element positions and sizes\n --depth dom tree depth (default: auto, max 20)\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
30
60
  "flags": {
31
61
  "json": "boolean",
32
62
  "brief": "boolean",
33
63
  "layout": "boolean",
34
- "depth": "string"
64
+ "depth": "string",
65
+ "frame": "string"
35
66
  },
36
67
  "args": [
37
68
  "selector"
38
69
  ]
39
70
  },
40
71
  "tree": {
41
- "help": "cssprobe-cli tree <selector>\n\nShow DOM tree structure\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --depth tree depth (default: auto, max 20)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
72
+ "help": "cssprobe-cli tree <selector>\n\nShow DOM tree structure\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --depth tree depth (default: auto, max 20)\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
42
73
  "flags": {
43
- "depth": "string"
74
+ "depth": "string",
75
+ "frame": "string"
44
76
  },
45
77
  "args": [
46
78
  "selector"
47
79
  ]
48
80
  },
49
81
  "layout": {
50
- "help": "cssprobe-cli layout <selector>\n\nShow ASCII layout diagram\n\nArguments:\n <selector> css selector for the root element\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
51
- "flags": {},
82
+ "help": "cssprobe-cli layout <selector>\n\nShow ASCII layout diagram\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
83
+ "flags": {
84
+ "frame": "string"
85
+ },
52
86
  "args": [
53
87
  "selector"
54
88
  ]
55
89
  },
56
90
  "findings": {
57
- "help": "cssprobe-cli findings <selector>\n\nShow only issues/warnings/errors\n\nArguments:\n <selector> css selector for the root element\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
58
- "flags": {},
91
+ "help": "cssprobe-cli findings <selector>\n\nShow only issues/warnings/errors\n\nArguments:\n <selector> css selector for the root element\nOptions:\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
92
+ "flags": {
93
+ "frame": "string"
94
+ },
59
95
  "args": [
60
96
  "selector"
61
97
  ]
62
98
  },
63
99
  "inject-css": {
64
- "help": "cssprobe-cli inject-css <css>\n\nInject CSS into the current page\n\nArguments:\n <css> css code to inject\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
65
- "flags": {},
100
+ "help": "cssprobe-cli inject-css <css>\n\nInject CSS into the current page\n\nArguments:\n <css> css code to inject\nOptions:\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
101
+ "flags": {
102
+ "frame": "string"
103
+ },
66
104
  "args": [
67
105
  "css"
68
106
  ]
@@ -76,14 +114,16 @@
76
114
  ]
77
115
  },
78
116
  "eval": {
79
- "help": "cssprobe-cli eval <expression>\n\nEvaluate JavaScript expression on the page (browser context)\n\nArguments:\n <expression> javascript expression to evaluate\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
80
- "flags": {},
117
+ "help": "cssprobe-cli eval <expression>\n\nEvaluate JavaScript expression on the page (browser context)\n\nArguments:\n <expression> javascript expression, or @file / - (stdin) for long scripts\nOptions:\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
118
+ "flags": {
119
+ "frame": "string"
120
+ },
81
121
  "args": [
82
122
  "expression"
83
123
  ]
84
124
  },
85
125
  "playwright": {
86
- "help": "cssprobe-cli playwright <call>\n\nExecute Playwright API call (Node.js context)\n\nArguments:\n <call> playwright api call, e.g. \"page.setviewportsize({width: 1280, height: 720})\"\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
126
+ "help": "cssprobe-cli playwright <call>\n\nExecute Playwright API call (Node.js context)\n\nArguments:\n <call> playwright api call, or @file / - (stdin); e.g. \"page.setviewportsize({width: 1280, height: 720})\"\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
87
127
  "flags": {},
88
128
  "args": [
89
129
  "call"
@@ -146,6 +186,33 @@
146
186
  "selector"
147
187
  ]
148
188
  },
189
+ "sample": {
190
+ "help": "cssprobe-cli sample <expression>\n\nSample a JS expression over time and return a timeline with summary stats\n\nArguments:\n <expression> js expression sampled on an interval\nOptions:\n --interval sampling interval in ms (default 250)\n --duration total sampling duration in ms (default 5000)\n --key dot path for numeric extraction, e.g. \"top\" or \"box.y\"\n --tail-from tail starts at this elapsed ms (default: last 5000ms)\n --ignore-errors record evaluation errors as sample points instead of aborting\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
191
+ "flags": {
192
+ "interval": "string",
193
+ "duration": "string",
194
+ "key": "string",
195
+ "tail-from": "string",
196
+ "ignore-errors": "boolean",
197
+ "frame": "string"
198
+ },
199
+ "args": [
200
+ "expression"
201
+ ]
202
+ },
203
+ "wait-stable": {
204
+ "help": "cssprobe-cli wait-stable <expression>\n\nWait until a JS expression stops changing (tolerance + stable window)\n\nArguments:\n <expression> js expression polled until stable\nOptions:\n --tolerance absolute tolerance for numeric values (default 0)\n --window stable window in ms (default 1500)\n --timeout timeout in ms (default 30000)\n --interval polling interval in ms (default 200)\n --frame target frame: index, name, or url substring (default: main frame)\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
205
+ "flags": {
206
+ "tolerance": "string",
207
+ "window": "string",
208
+ "timeout": "string",
209
+ "interval": "string",
210
+ "frame": "string"
211
+ },
212
+ "args": [
213
+ "expression"
214
+ ]
215
+ },
149
216
  "run": {
150
217
  "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
218
  "flags": {
@@ -173,6 +240,14 @@
173
240
  },
174
241
  "args": []
175
242
  },
243
+ "state-check": {
244
+ "help": "cssprobe-cli state-check \n\nValidate a saved state file: cookies, expiry, and target domain coverage\n\nOptions:\n --state state file path (default: ~/.cssprobe-cli/states/session.json)\n --url target url whose hostname must be covered by the cookies\n\nGlobal options:\n --json output as JSON\n --profile, -p <name> use named profile",
245
+ "flags": {
246
+ "state": "string",
247
+ "url": "string"
248
+ },
249
+ "args": []
250
+ },
176
251
  "config-show": {
177
252
  "help": "cssprobe-cli config-show \n\nShow current configuration\n\nOptions:\n --profile profile name",
178
253
  "flags": {
@@ -256,7 +331,9 @@
256
331
  },
257
332
  "booleanOptions": [
258
333
  "headed",
334
+ "fail-on-login",
259
335
  "all",
336
+ "bail",
260
337
  "json",
261
338
  "brief",
262
339
  "layout",
@@ -265,6 +342,7 @@
265
342
  "sequential",
266
343
  "clear",
267
344
  "network-idle",
345
+ "ignore-errors",
268
346
  "local"
269
347
  ]
270
348
  }
@@ -0,0 +1,13 @@
1
+ import type { AssertStep } from './schema';
2
+ export interface AssertionState {
3
+ url: string;
4
+ title: string;
5
+ frames: number;
6
+ }
7
+ export interface AssertionContext {
8
+ vars: Record<string, unknown>;
9
+ state: AssertionState;
10
+ globals: Record<string, string>;
11
+ }
12
+ export declare function resolvePath(value: unknown, path?: string): unknown;
13
+ export declare function evaluateAssertion(assertion: AssertStep, context: AssertionContext): string | null;